From f1f7765cf5eea45b9f88f4a9b78568644d297c37 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 4 Sep 2024 16:38:30 +0200 Subject: [PATCH 01/56] Fix invalidated entries counter metric (#5890) --- apollo-router/src/plugins/cache/invalidation.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apollo-router/src/plugins/cache/invalidation.rs b/apollo-router/src/plugins/cache/invalidation.rs index a566dc6f33..2f0b71862b 100644 --- a/apollo-router/src/plugins/cache/invalidation.rs +++ b/apollo-router/src/plugins/cache/invalidation.rs @@ -173,13 +173,12 @@ async fn handle_request( .map(|k| RedisKey(k.to_string())) .collect::>(); if !keys.is_empty() { - count += keys.len() as u64; - storage.delete(keys).await; + count += storage.delete(keys).await.unwrap_or(0) as u64; u64_counter!( "apollo.router.operations.entity.invalidation.entry", "Entity cache counter for invalidated entries", - 1u64, + count, "origin" = origin, "subgraph.name" = subgraph.clone() ); From 22fb29ea95bf659d96ec818994bb5d9794455b19 Mon Sep 17 00:00:00 2001 From: Tyler Bloom Date: Wed, 4 Sep 2024 13:03:26 -0400 Subject: [PATCH 02/56] Moves tests for `FallibleIterator` (#5955) --- .../src/utils/fallible_iterator.rs | 192 ++++++++++++------ apollo-federation/src/utils/mod.rs | 1 + 2 files changed, 133 insertions(+), 60 deletions(-) diff --git a/apollo-federation/src/utils/fallible_iterator.rs b/apollo-federation/src/utils/fallible_iterator.rs index 83564c74f6..36dc23b845 100644 --- a/apollo-federation/src/utils/fallible_iterator.rs +++ b/apollo-federation/src/utils/fallible_iterator.rs @@ -77,9 +77,7 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// the iterator will yield the `Err` in its place. Lastly, if the predicate yields `Ok(true)`, /// the iterator will yield `Ok(val)`. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// + /// ```ignore /// // A totally accurate prime checker /// fn is_prime(i: usize) -> Result { /// match i { @@ -113,9 +111,7 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// method is very similar to `Itertools::filter_ok` except the predicate for this method is /// fallible. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// + /// ```ignore /// // A totally accurate prime checker /// fn is_prime(i: usize) -> Result { /// match i { @@ -147,9 +143,7 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// `Ok(false)`, the returned value will be `Ok(false)`. If that item is `Err`, than that `Err` /// is returned. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// + /// ```ignore /// // A totally accurate prime checker /// fn is_prime(i: usize) -> Result { /// match i { @@ -188,19 +182,11 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// is `Ok`, it is given to the predicate. If the predicate returns `false`, this method /// returns `Ok(false)`. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// - /// type Item = Result; - /// - /// fn is_even(i: usize) -> bool { - /// i % 2 == 0 - /// } - /// - /// let first_values: Vec = vec![]; - /// let second_values: Vec = vec![Ok(1), Err(())]; - /// let third_values: Vec = vec![Ok(0), Ok(1), Ok(2)]; - /// let fourth_values: Vec = vec![Err(()), Ok(0)]; + /// ```ignore + /// let first_values = vec![]; + /// let second_values = vec![Ok(1), Err(())]; + /// let third_values = vec![Ok(0), Ok(1), Ok(2)]; + /// let fourth_values = vec![Err(()), Ok(0)]; /// /// assert_eq!(Ok(true), first_values.into_iter().ok_and_all(is_even)); /// assert_eq!(Ok(false), second_values.into_iter().ok_and_all(is_even)); @@ -224,11 +210,7 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// predicate returns `Err`, that `Err` is returned. If the predicate returns `Ok(false)`, /// `Ok(false)` is returned. By default, this function returned `Ok(true)`. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// - /// type Item = Result; - /// + /// ```ignore /// // A totally accurate prime checker /// fn is_prime(i: usize) -> Result { /// match i { @@ -238,12 +220,12 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// } /// } /// - /// let first_values: Vec = vec![]; - /// let second_values: Vec = vec![Ok(0), Err(())]; - /// let third_values: Vec = vec![Ok(2), Ok(3)]; - /// let fourth_values: Vec = vec![Err(()), Ok(2)]; - /// let fifth_values: Vec = vec![Ok(2), Err(())]; - /// let sixth_values: Vec = vec![Ok(4), Ok(3)]; + /// let first_values = vec![]; + /// let second_values = vec![Ok(0), Err(())]; + /// let third_values = vec![Ok(2), Ok(3)]; + /// let fourth_values = vec![Err(()), Ok(2)]; + /// let fifth_values = vec![Ok(2), Err(())]; + /// let sixth_values = vec![Ok(4), Ok(3)]; /// /// assert_eq!(Ok(true), first_values.into_iter().and_then_all(is_prime)); /// assert_eq!(Err(()), second_values.into_iter().and_then_all(is_prime)); @@ -275,9 +257,7 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// `Ok(true)`, the returned value will be `Ok(true)`. If that item is `Err`, than that `Err` /// is returned. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// + /// ```ignore /// // A totally accurate prime checker /// fn is_prime(i: usize) -> Result { /// match i { @@ -316,19 +296,11 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// is `Ok`, it is given to the predicate. If the predicate returns `true`, this method returns /// `Ok(true)`. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// - /// type Item = Result; - /// - /// fn is_even(i: usize) -> bool { - /// i % 2 == 0 - /// } - /// - /// let first_values: Vec = vec![]; - /// let second_values: Vec = vec![Ok(0), Err(())]; - /// let third_values: Vec = vec![Ok(1), Ok(3)]; - /// let fourth_values: Vec = vec![Err(()), Ok(0)]; + /// ```ignore + /// let first_values = vec![]; + /// let second_values = vec![Ok(0), Err(())]; + /// let third_values = vec![Ok(1), Ok(3)]; + /// let fourth_values = vec![Err(()), Ok(0)]; /// /// assert_eq!(Ok(false), first_values.into_iter().ok_and_any(is_even)); /// assert_eq!(Ok(true), second_values.into_iter().ok_and_any(is_even)); @@ -352,11 +324,7 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// predicate returns `Err`, that `Err` is returned. If the predicate returns `Ok(true)`, /// `Ok(true)` is returned. By default, this function returned `Ok(false)`. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// - /// type Item = Result; - /// + /// ```ignore /// // A totally accurate prime checker /// fn is_prime(i: usize) -> Result { /// match i { @@ -366,12 +334,12 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// } /// } /// - /// let first_values: Vec = vec![]; - /// let second_values: Vec = vec![Ok(0), Err(())]; - /// let third_values: Vec = vec![Ok(3), Ok(4)]; - /// let fourth_values: Vec = vec![Err(()), Ok(2)]; - /// let fifth_values: Vec = vec![Ok(2), Err(())]; - /// let sixth_values: Vec = vec![Ok(4), Ok(5)]; + /// let first_values = vec![]; + /// let second_values = vec![Ok(0), Err(())]; + /// let third_values = vec![Ok(3), Ok(4)]; + /// let fourth_values = vec![Err(()), Ok(2)]; + /// let fifth_values = vec![Ok(2), Err(())]; + /// let sixth_values = vec![Ok(4), Ok(5)]; /// /// assert_eq!(Ok(false), first_values.into_iter().and_then_any(is_prime)); /// assert_eq!(Err(()), second_values.into_iter().and_then_any(is_prime)); @@ -527,3 +495,107 @@ where self.iter.next().map(|res| res.or_else(&mut self.map)) } } + +// Ideally, these would be doc tests, but gating access to the `utils` module is messy. +#[cfg(test)] +mod tests { + use super::*; + + type Item = Result; + + fn is_prime(i: usize) -> Result { + match i { + 0 | 1 => Err(()), // 0 and 1 are neither prime or composite + 2 | 3 => Ok(true), + _ => Ok(false), // Every other number is composite, I guess + } + } + + fn is_even(i: usize) -> bool { + i % 2 == 0 + } + + fn test_fallible_filter() { + let vals = (1..6).fallible_filter(|i| is_prime(*i)); + itertools::assert_equal(vals, vec![Err(()), Ok(2), Ok(3)]); + } + + fn test_and_then_filter() { + let vals = vec![Ok(0), Err(()), Err(()), Ok(3), Ok(4)] + .into_iter() + .and_then_filter(|i| is_prime(*i)); + itertools::assert_equal(vals, vec![Err(()), Err(()), Err(()), Ok(3)]); + } + + fn test_fallible_all() { + assert_eq!(Ok(true), [].into_iter().fallible_all(is_prime)); + assert_eq!(Ok(true), (2..4).fallible_all(is_prime)); + assert_eq!(Err(()), (1..4).fallible_all(is_prime)); + assert_eq!(Ok(false), (2..5).fallible_all(is_prime)); + assert_eq!(Err(()), (1..5).fallible_all(is_prime)); + } + + fn test_ok_and_all() { + let first_values: Vec = vec![]; + let second_values: Vec = vec![Ok(1), Err(())]; + let third_values: Vec = vec![Ok(0), Ok(1), Ok(2)]; + let fourth_values: Vec = vec![Err(()), Ok(0)]; + + assert_eq!(Ok(true), first_values.into_iter().ok_and_all(is_even)); + assert_eq!(Ok(false), second_values.into_iter().ok_and_all(is_even)); + assert_eq!(Ok(false), third_values.into_iter().ok_and_all(is_even)); + assert_eq!(Err(()), fourth_values.into_iter().ok_and_all(is_even)); + } + + fn test_and_then_all() { + let first_values: Vec = vec![]; + let second_values: Vec = vec![Ok(0), Err(())]; + let third_values: Vec = vec![Ok(2), Ok(3)]; + let fourth_values: Vec = vec![Err(()), Ok(2)]; + let fifth_values: Vec = vec![Ok(2), Err(())]; + let sixth_values: Vec = vec![Ok(4), Ok(3)]; + + assert_eq!(Ok(true), first_values.into_iter().and_then_all(is_prime)); + assert_eq!(Err(()), second_values.into_iter().and_then_all(is_prime)); + assert_eq!(Ok(true), third_values.into_iter().and_then_all(is_prime)); + assert_eq!(Err(()), fourth_values.into_iter().and_then_all(is_prime)); + assert_eq!(Err(()), fifth_values.into_iter().and_then_all(is_prime)); + assert_eq!(Ok(false), sixth_values.into_iter().and_then_all(is_prime)); + } + + fn test_fallible_any() { + assert_eq!(Ok(false), [].into_iter().fallible_any(is_prime)); + assert_eq!(Ok(true), (2..5).fallible_any(is_prime)); + assert_eq!(Ok(false), (4..5).fallible_any(is_prime)); + assert_eq!(Err(()), (1..4).fallible_any(is_prime)); + assert_eq!(Err(()), (1..5).fallible_any(is_prime)); + } + + fn test_ok_and_any() { + let first_values: Vec = vec![]; + let second_values: Vec = vec![Ok(0), Err(())]; + let third_values: Vec = vec![Ok(1), Ok(3)]; + let fourth_values: Vec = vec![Err(()), Ok(0)]; + + assert_eq!(Ok(false), first_values.into_iter().ok_and_any(is_even)); + assert_eq!(Ok(true), second_values.into_iter().ok_and_any(is_even)); + assert_eq!(Ok(false), third_values.into_iter().ok_and_any(is_even)); + assert_eq!(Err(()), fourth_values.into_iter().ok_and_any(is_even)); + } + + fn test_and_then_any() { + let first_values: Vec = vec![]; + let second_values: Vec = vec![Ok(0), Err(())]; + let third_values: Vec = vec![Ok(3), Ok(4)]; + let fourth_values: Vec = vec![Err(()), Ok(2)]; + let fifth_values: Vec = vec![Ok(2), Err(())]; + let sixth_values: Vec = vec![Ok(4), Ok(5)]; + + assert_eq!(Ok(false), first_values.into_iter().and_then_any(is_prime)); + assert_eq!(Err(()), second_values.into_iter().and_then_any(is_prime)); + assert_eq!(Ok(true), third_values.into_iter().and_then_any(is_prime)); + assert_eq!(Err(()), fourth_values.into_iter().and_then_any(is_prime)); + assert_eq!(Ok(true), fifth_values.into_iter().and_then_any(is_prime)); + assert_eq!(Ok(false), sixth_values.into_iter().and_then_any(is_prime)); + } +} diff --git a/apollo-federation/src/utils/mod.rs b/apollo-federation/src/utils/mod.rs index ec910a87f8..fe4b815704 100644 --- a/apollo-federation/src/utils/mod.rs +++ b/apollo-federation/src/utils/mod.rs @@ -2,4 +2,5 @@ mod fallible_iterator; pub mod logging; + pub(crate) use fallible_iterator::*; From 19c146184159379ca85ed54b894c1cafc73fe338 Mon Sep 17 00:00:00 2001 From: Coenen Benjamin Date: Thu, 5 Sep 2024 14:38:28 +0200 Subject: [PATCH 03/56] fix trace integration test (#5962) Signed-off-by: Benjamin Coenen <5719034+bnjjj@users.noreply.github.com> --- .../src/plugins/telemetry/otel/layer.rs | 1 - .../tests/integration/telemetry/datadog.rs | 18 +++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/apollo-router/src/plugins/telemetry/otel/layer.rs b/apollo-router/src/plugins/telemetry/otel/layer.rs index e1d20ec739..866bf50a35 100644 --- a/apollo-router/src/plugins/telemetry/otel/layer.rs +++ b/apollo-router/src/plugins/telemetry/otel/layer.rs @@ -1101,7 +1101,6 @@ where attributes.insert(OTEL_ORIGINAL_NAME.into(), builder.name.into()); builder.name = forced_span_name.into(); } - // Assign end time, build and start span, drop span to export builder .with_end_time(SystemTime::now()) diff --git a/apollo-router/tests/integration/telemetry/datadog.rs b/apollo-router/tests/integration/telemetry/datadog.rs index a88f16160b..d2d3f58d25 100644 --- a/apollo-router/tests/integration/telemetry/datadog.rs +++ b/apollo-router/tests/integration/telemetry/datadog.rs @@ -52,6 +52,7 @@ async fn test_default_span_names() -> Result<(), BoxError> { .unwrap(), id.to_datadog() ); + router.graceful_shutdown().await; TraceSpec::builder() .services(["client", "router", "subgraph"].into()) .span_names( @@ -73,7 +74,6 @@ async fn test_default_span_names() -> Result<(), BoxError> { .build() .validate_trace(id) .await?; - router.graceful_shutdown().await; Ok(()) } @@ -104,6 +104,7 @@ async fn test_override_span_names() -> Result<(), BoxError> { .unwrap(), id.to_datadog() ); + router.graceful_shutdown().await; TraceSpec::builder() .services(["client", "router", "subgraph"].into()) .span_names( @@ -125,7 +126,6 @@ async fn test_override_span_names() -> Result<(), BoxError> { .build() .validate_trace(id) .await?; - router.graceful_shutdown().await; Ok(()) } @@ -156,6 +156,7 @@ async fn test_override_span_names_late() -> Result<(), BoxError> { .unwrap(), id.to_datadog() ); + router.graceful_shutdown().await; TraceSpec::builder() .services(["client", "router", "subgraph"].into()) .span_names( @@ -177,7 +178,6 @@ async fn test_override_span_names_late() -> Result<(), BoxError> { .build() .validate_trace(id) .await?; - router.graceful_shutdown().await; Ok(()) } @@ -206,6 +206,7 @@ async fn test_basic() -> Result<(), BoxError> { .unwrap(), id.to_datadog() ); + router.graceful_shutdown().await; TraceSpec::builder() .operation_name("ExampleQuery") .services(["client", "router", "subgraph"].into()) @@ -240,7 +241,6 @@ async fn test_basic() -> Result<(), BoxError> { .build() .validate_trace(id) .await?; - router.graceful_shutdown().await; Ok(()) } @@ -274,6 +274,7 @@ async fn test_with_parent_span() -> Result<(), BoxError> { .unwrap(), id.to_datadog() ); + router.graceful_shutdown().await; TraceSpec::builder() .operation_name("ExampleQuery") .services(["client", "router", "subgraph"].into()) @@ -308,7 +309,6 @@ async fn test_with_parent_span() -> Result<(), BoxError> { .build() .validate_trace(id) .await?; - router.graceful_shutdown().await; Ok(()) } @@ -383,6 +383,7 @@ async fn test_resource_mapping_override() -> Result<(), BoxError> { .get("apollo-custom-trace-id") .unwrap() .is_empty()); + router.graceful_shutdown().await; TraceSpec::builder() .services(["client", "router", "subgraph"].into()) .span_names( @@ -403,7 +404,6 @@ async fn test_resource_mapping_override() -> Result<(), BoxError> { .build() .validate_trace(id) .await?; - router.graceful_shutdown().await; Ok(()) } @@ -428,6 +428,7 @@ async fn test_span_metrics() -> Result<(), BoxError> { .get("apollo-custom-trace-id") .unwrap() .is_empty()); + router.graceful_shutdown().await; TraceSpec::builder() .operation_name("ExampleQuery") .services(["client", "router", "subgraph"].into()) @@ -450,7 +451,6 @@ async fn test_span_metrics() -> Result<(), BoxError> { .build() .validate_trace(id) .await?; - router.graceful_shutdown().await; Ok(()) } @@ -495,12 +495,12 @@ impl TraceSpec { .await?; tracing::debug!("{}", serde_json::to_string_pretty(&trace)?); self.verify_trace_participants(&trace)?; + self.verify_spans_present(&trace)?; + self.validate_measured_spans(&trace)?; self.verify_operation_name(&trace)?; self.verify_priority_sampled(&trace)?; self.verify_version(&trace)?; - self.verify_spans_present(&trace)?; self.validate_span_kinds(&trace)?; - self.validate_measured_spans(&trace)?; Ok(()) } From f4b4002655e87a67510ec076dea790fc5a007896 Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:01:36 -0500 Subject: [PATCH 04/56] fix(federation): always update conditions on FetchSelectionSet updates (#5950) In JS implementation, conditions were recalculated at the very end when we were creating final selection set. We were missing condition update in one of the update paths. Co-authored-by: Tyler Bloom --- .../src/query_plan/fetch_dependency_graph.rs | 37 ++- .../query_plan/build_query_plan_tests.rs | 220 +++++++++++++++++- ...tiple_conditions_on_abstract_types.graphql | 106 +++++++++ 3 files changed, 359 insertions(+), 4 deletions(-) create mode 100644 apollo-federation/tests/query_plan/supergraphs/handles_multiple_conditions_on_abstract_types.graphql diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index de9a3a87c3..a8368f6f74 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -2478,6 +2478,38 @@ impl FetchDependencyGraphNode { _ => err, })?; + // this function removes unnecessary pieces of the query plan requires selection set. + // PORT NOTE: this function was called trimSelectioNodes in the JS implementation + fn trim_requires_selection_set( + selection_set: &executable::SelectionSet, + ) -> Vec { + selection_set + .selections + .iter() + .filter_map(|s| match s { + executable::Selection::Field(field) => Some(executable::Selection::from( + executable::Field::new(field.name.clone(), field.definition.clone()) + .with_selections(trim_requires_selection_set(&field.selection_set)), + )), + executable::Selection::InlineFragment(inline_fragment) => { + let new_fragment = inline_fragment + .type_condition + .clone() + .map(executable::InlineFragment::with_type_condition) + .unwrap_or_else(|| { + executable::InlineFragment::without_type_condition( + inline_fragment.selection_set.ty.clone(), + ) + }) + .with_selections(trim_requires_selection_set( + &inline_fragment.selection_set, + )); + Some(executable::Selection::from(new_fragment)) + } + executable::Selection::FragmentSpread(_) => None, + }) + .collect() + } let node = super::PlanNode::Fetch(Box::new(super::FetchNode { subgraph_name: self.subgraph_name.clone(), id: self.id.get().copied(), @@ -2486,7 +2518,7 @@ impl FetchDependencyGraphNode { .as_ref() .map(executable::SelectionSet::try_from) .transpose()? - .map(|selection_set| selection_set.selections), + .map(|selection_set| trim_requires_selection_set(&selection_set)), operation_document, operation_name, operation_kind: self.root_kind.into(), @@ -2881,6 +2913,9 @@ impl FetchSelectionSet { fn add_selections(&mut self, selection_set: &Arc) -> Result<(), FederationError> { Arc::make_mut(&mut self.selection_set).add_selection_set(selection_set)?; + // TODO: when calling this multiple times, maybe only re-compute conditions at the end? + // Or make it lazily-initialized and computed on demand? + self.conditions = self.selection_set.conditions()?; Ok(()) } } 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 8f256843e2..0d618a6d0f 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests.rs @@ -474,10 +474,8 @@ fn it_executes_mutation_operations_in_sequence() { ); } -/// @requires references external field indirectly { +/// @requires references external field indirectly #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure (appears to be visiting wrong subgraph) fn key_where_at_external_is_not_at_top_level_of_selection_of_requires() { // Field issue where we were seeing a FetchGroup created where the fields used by the key to jump subgraphs // were not properly fetched. In the below test, this test will ensure that 'k2' is properly collected @@ -1079,3 +1077,219 @@ fn test_merging_fetches_reset_cached_costs() { "### ); } + +#[test] +fn handles_multiple_conditions_on_abstract_types() { + let planner = planner!( + books: r#" + type Book @key(fields: "id") { + id: ID! + title: String + } + "#, + magazines: r#" + type Magazine @key(fields: "id") { + id: ID! + title: String + } + "#, + products: r#" + type Query { + products: [Product] + } + + interface Product { + id: ID! + sku: String + dimensions: ProductDimension + } + + type ProductDimension @shareable { + size: String + weight: Float + } + + type Book implements Product @key(fields: "id") { + id: ID! + sku: String + dimensions: ProductDimension @shareable + } + + type Magazine implements Product @key(fields: "id") { + id: ID! + sku: String + dimensions: ProductDimension @shareable + } + "#, + reviews: r#" + type Book implements Product @key(fields: "id") { + id: ID! + reviews: [Review!]! + } + + type Magazine implements Product @key(fields: "id") { + id: ID! + reviews: [Review!]! + } + + interface Product { + id: ID! + reviews: [Review!]! + } + + type Review { + id: Int! + body: String! + product: Product + } + "#, + ); + + assert_plan!( + &planner, + r#" + query ($title: Boolean = true) { + products { + id + reviews { + product { + id + ... on Book @include(if: $title) { + title + ... on Book @skip(if: $title) { + sku + } + } + ... on Magazine { + sku + } + } + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "products") { + { + products { + __typename + id + ... on Book { + __typename + id + } + ... on Magazine { + __typename + id + } + } + } + }, + Flatten(path: "products.@") { + Fetch(service: "reviews") { + { + ... on Book { + __typename + id + } + ... on Magazine { + __typename + id + } + } => + { + ... on Book { + reviews { + product { + __typename + id + ... on Book @include(if: $title) { + __typename + id + ... on Book @skip(if: $title) { + __typename + id + } + } + ... on Magazine { + __typename + id + } + } + } + } + ... on Magazine { + reviews { + product { + __typename + id + ... on Book @include(if: $title) { + __typename + id + ... on Book @skip(if: $title) { + __typename + id + } + } + ... on Magazine { + __typename + id + } + } + } + } + } + }, + }, + Parallel { + Flatten(path: "products.@.reviews.@.product") { + Fetch(service: "products") { + { + ... on Book { + ... on Book { + __typename + id + } + } + ... on Magazine { + __typename + id + } + } => + { + ... on Book @skip(if: $title) { + ... on Book @include(if: $title) { + sku + } + } + ... on Magazine { + sku + } + } + }, + }, + Include(if: $title) { + Flatten(path: "products.@.reviews.@.product") { + Fetch(service: "books") { + { + ... on Book { + __typename + id + } + } => + { + ... on Book { + title + } + } + }, + }, + }, + }, + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_multiple_conditions_on_abstract_types.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_conditions_on_abstract_types.graphql new file mode 100644 index 0000000000..2965317df8 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_conditions_on_abstract_types.graphql @@ -0,0 +1,106 @@ +# Composed from subgraphs with hash: 1b8ed03436d12bd5fe1dc3487873cc027b7f81da +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type Book implements Product + @join__implements(graph: PRODUCTS, interface: "Product") + @join__implements(graph: REVIEWS, interface: "Product") + @join__type(graph: BOOKS, key: "id") + @join__type(graph: PRODUCTS, key: "id") + @join__type(graph: REVIEWS, key: "id") +{ + id: ID! + title: String @join__field(graph: BOOKS) + sku: String @join__field(graph: PRODUCTS) + dimensions: ProductDimension @join__field(graph: PRODUCTS) + reviews: [Review!]! @join__field(graph: REVIEWS) +} + +scalar join__FieldSet + +enum join__Graph { + BOOKS @join__graph(name: "books", url: "none") + MAGAZINES @join__graph(name: "magazines", url: "none") + PRODUCTS @join__graph(name: "products", url: "none") + REVIEWS @join__graph(name: "reviews", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Magazine implements Product + @join__implements(graph: PRODUCTS, interface: "Product") + @join__implements(graph: REVIEWS, interface: "Product") + @join__type(graph: MAGAZINES, key: "id") + @join__type(graph: PRODUCTS, key: "id") + @join__type(graph: REVIEWS, key: "id") +{ + id: ID! + title: String @join__field(graph: MAGAZINES) + sku: String @join__field(graph: PRODUCTS) + dimensions: ProductDimension @join__field(graph: PRODUCTS) + reviews: [Review!]! @join__field(graph: REVIEWS) +} + +interface Product + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) +{ + id: ID! + sku: String @join__field(graph: PRODUCTS) + dimensions: ProductDimension @join__field(graph: PRODUCTS) + reviews: [Review!]! @join__field(graph: REVIEWS) +} + +type ProductDimension + @join__type(graph: PRODUCTS) +{ + size: String + weight: Float +} + +type Query + @join__type(graph: BOOKS) + @join__type(graph: MAGAZINES) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) +{ + products: [Product] @join__field(graph: PRODUCTS) +} + +type Review + @join__type(graph: REVIEWS) +{ + id: Int! + body: String! + product: Product +} From 56df80d384f6ef7a7dab6905b6a4f42a374fc8fa Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Thu, 5 Sep 2024 09:17:12 -0600 Subject: [PATCH 05/56] docs: Run everything in parallel when releasing (#5945) We may burn additional CI costs for this release job, but that's acceptable. Tests will still run and we'll be okay. Linting doesn't matter. --- .circleci/config.yml | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0d901fbeb9..1d0e41f62c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1102,10 +1102,6 @@ workflows: tags: only: /v.*/ - test_updated: - requires: - - lint - - check_helm - - check_compliance matrix: parameters: platform: @@ -1116,10 +1112,6 @@ workflows: tags: only: /v.*/ - test: - requires: - - lint - - check_helm - - check_compliance matrix: parameters: platform: @@ -1130,10 +1122,6 @@ workflows: tags: only: /v.*/ - build_release: - requires: - - pre_verify_release - - test - - test_updated matrix: parameters: platform: @@ -1144,7 +1132,14 @@ workflows: tags: only: /v.*/ - publish_github_release: - requires: [ build_release ] + requires: + - build_release + - lint + - check_helm + - check_compliance + - pre_verify_release + - test + - test_updated filters: branches: ignore: /.*/ From 68305b80fb778bf15e875a9705ac233ca708f1e7 Mon Sep 17 00:00:00 2001 From: Tyler Bloom Date: Fri, 6 Sep 2024 04:40:08 -0400 Subject: [PATCH 06/56] fix(fed): corrected insertion logic in `add_at_path` (#5963) In a recent change that added logic in `add_at_path` to remove unnecessary directives from selections, the insertion logic into the selection set had a bug. If the set did not have the key of the `OpPathElement`, we would generate a selection from the element after removing unnecessary directives. However, this was using the `entry` API, which requires that the key of the `Selection` passed to `.or_insert` matches the original. Because we removed the unnecessary directives, these keys were prone to mismatching. Co-authored-by: Iryna Shestak --- apollo-federation/src/operation/mod.rs | 59 ++++--- .../src/query_graph/graph_path.rs | 23 ++- .../requires/include_skip.rs | 156 ++++++++++++++++++ ...rwritten_after_removing_directives.graphql | 65 ++++++++ ...include_is_stripped_from_fragments.graphql | 64 +++++++ 5 files changed, 333 insertions(+), 34 deletions(-) create mode 100644 apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql diff --git a/apollo-federation/src/operation/mod.rs b/apollo-federation/src/operation/mod.rs index 0b04790556..4c17083c3b 100644 --- a/apollo-federation/src/operation/mod.rs +++ b/apollo-federation/src/operation/mod.rs @@ -20,6 +20,7 @@ use std::ops::Deref; use std::sync::atomic; use std::sync::Arc; +use apollo_compiler::collections::HashSet; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::executable; @@ -34,7 +35,6 @@ use crate::compat::coerce_executable_values; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::error::SingleFederationError::Internal; -use crate::query_graph::graph_path::op_slice_condition_directives; use crate::query_graph::graph_path::OpPathElement; use crate::query_plan::conditions::Conditions; use crate::query_plan::FetchDataKeyRenamer; @@ -673,8 +673,8 @@ mod selection_map { if *self.key() != value.key() { return Err(Internal { message: format!( - "Key mismatch when inserting selection {} into vacant entry ", - value + "Key mismatch when inserting selection `{value}` into vacant entry. Expected {:?}, found {:?}", + self.key(), value.key() ), } .into()); @@ -789,7 +789,7 @@ impl Selection { pub(crate) fn from_element( element: OpPathElement, sub_selections: Option, - unnecessary_directives: Option<&DirectiveList>, + unnecessary_directives: Option<&HashSet>>, ) -> Result { // PORT_NOTE: This is TODO item is copied from the JS `selectionOfElement` function. // TODO: validate that the subSelection is ok for the element @@ -805,7 +805,7 @@ impl Selection { let directives = inline_fragment .directives .iter() - .filter(|dir| !unnecessary_directives.contains(dir)) + .filter(|dir| !unnecessary_directives.contains(dir.as_ref())) .cloned() .collect::(); Ok(InlineFragmentSelection::new( @@ -2626,6 +2626,16 @@ impl SelectionSet { &mut self, path: &[Arc], selection_set: Option<&Arc>, + ) -> Result<(), FederationError> { + let mut unnecessary_directives = HashSet::default(); + self.add_at_path_inner(path, selection_set, &mut unnecessary_directives) + } + + fn add_at_path_inner( + &mut self, + path: &[Arc], + selection_set: Option<&Arc>, + unnecessary_directives: &mut HashSet>, ) -> Result<(), FederationError> { // PORT_NOTE: This method was ported from the JS class `SelectionSetUpdates`. Unlike the // JS code, this mutates the selection set map in-place. @@ -2636,30 +2646,39 @@ impl SelectionSet { let Some(sub_selection_type) = element.sub_selection_type_position()? else { return Err(FederationError::internal("unexpected error: add_at_path encountered a field that is not of a composite type".to_string())); }; - let mut selection = Arc::make_mut(&mut self.selections) - .entry(ele.key()) - .or_insert(|| { - let unnecessary_directives = op_slice_condition_directives(path); - Selection::from_element( + let target = Arc::make_mut(&mut self.selections); + let mut selection = match target.get_mut(&ele.key()) { + Some(selection) => selection, + None => { + let selection = Selection::from_element( element, // We immediately add a selection afterward to make this selection set // valid. Some(SelectionSet::empty(self.schema.clone(), sub_selection_type)), - Some(&unnecessary_directives), - ) - })?; + Some(&*unnecessary_directives), + )?; + target.entry(selection.key()).or_insert(|| Ok(selection))? + } + }; + unnecessary_directives.extend( + selection + .get_directives_mut() + .iter() + .filter(|d| d.name == "include" || d.name == "skip") + .cloned(), + ); match &mut selection { SelectionValue::Field(field) => match field.get_selection_set_mut() { - Some(sub_selection) => sub_selection.add_at_path(path, selection_set)?, + Some(sub_selection) => sub_selection.add_at_path_inner(path, selection_set, unnecessary_directives), None => return Err(FederationError::internal("add_at_path encountered a field without a subselection which should never happen".to_string())), }, SelectionValue::InlineFragment(fragment) => fragment .get_selection_set_mut() - .add_at_path(path, selection_set)?, + .add_at_path_inner(path, selection_set, unnecessary_directives), SelectionValue::FragmentSpread(_fragment) => { - return Err(FederationError::internal("add_at_path encountered a named fragment spread which should never happen".to_string())); + return Err(FederationError::internal("add_at_path encountered a named fragment spread which should never happen".to_string())) } - }; + }?; } // If we have no sub-path, we can add the selection. Some((ele, &[])) => { @@ -2677,10 +2696,9 @@ impl SelectionSet { if !ele.is_terminal()? { return Ok(()); } else { - let unneeded_directives = op_slice_condition_directives(path); // add leaf let selection = - Selection::from_element(element, None, Some(&unneeded_directives))?; + Selection::from_element(element, None, Some(&*unnecessary_directives))?; self.add_local_selection(&selection, true)? } } else { @@ -2696,11 +2714,10 @@ impl SelectionSet { }) .transpose()? .map(|selection_set| selection_set.without_unnecessary_fragments()); - let unneeded_directives = op_slice_condition_directives(path); let selection = Selection::from_element( element, selection_set, - Some(&unneeded_directives), + Some(&*unnecessary_directives), )?; self.add_local_selection(&selection, true)?; } diff --git a/apollo-federation/src/query_graph/graph_path.rs b/apollo-federation/src/query_graph/graph_path.rs index 1cc8984509..b9486f8434 100644 --- a/apollo-federation/src/query_graph/graph_path.rs +++ b/apollo-federation/src/query_graph/graph_path.rs @@ -3656,18 +3656,6 @@ impl ClosedBranch { } } -pub fn op_slice_condition_directives(path: &[Arc]) -> DirectiveList { - path.iter() - .flat_map(|path_element| { - path_element - .directives() - .iter() - .filter(|d| d.name == "include" || d.name == "skip") - }) - .cloned() - .collect() -} - impl OpPath { pub fn len(&self) -> usize { self.0.len() @@ -3690,7 +3678,16 @@ impl OpPath { } pub(crate) fn conditional_directives(&self) -> DirectiveList { - op_slice_condition_directives(&self.0) + self.0 + .iter() + .flat_map(|path_element| { + path_element + .directives() + .iter() + .filter(|d| d.name == "include" || d.name == "skip") + }) + .cloned() + .collect() } /// Filter any fragment element in the provided path whose type condition does not exist in the provided schema. diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/requires/include_skip.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/requires/include_skip.rs index 7e5ad05772..ebb98c6653 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/requires/include_skip.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/requires/include_skip.rs @@ -238,3 +238,159 @@ fn it_handles_an_at_requires_where_multiple_conditional_are_involved() { "### ); } + +#[test] +fn unnecessary_include_is_stripped_from_fragments() { + let planner = planner!( + Subgraph1: r#" + type Query { + foo: Foo, + } + + type Foo @key(fields: "id") { + id: ID, + bar: Bar, + } + + type Bar @key(fields: "id") { + id: ID, + } + "#, + Subgraph2: r#" + type Bar @key(fields: "id") { + id: ID, + a: Int, + } + "#, + ); + assert_plan!( + &planner, + r#" + query foo($test: Boolean!) { + foo @include(if: $test) { + ... on Foo @include(if: $test) { + id + } + } + } + "#, + @r###" + QueryPlan { + Include(if: $test) { + Fetch(service: "Subgraph1") { + { + foo { + ... on Foo { + id + } + } + } + }, + }, + } + "### + ); + assert_plan!( + &planner, + r#" + query foo($test: Boolean!) { + foo @include(if: $test) { + ... on Foo @include(if: $test) { + id + bar { + ... on Bar @include(if: $test) { + id + } + } + } + } + } + "#, + @r###" + QueryPlan { + Include(if: $test) { + Fetch(service: "Subgraph1") { + { + foo { + ... on Foo { + id + bar { + ... on Bar { + id + } + } + } + } + } + }, + }, + } + "### + ); +} + +#[test] +fn selections_are_not_overwritten_after_removing_directives() { + let planner = planner!( + Subgraph1: r#" + type Query { + foo: Foo, + } + + type Foo @key(fields: "id") { + id: ID, + foo: Foo, + bar: Bar, + } + + type Bar @key(fields: "id") { + id: ID, + } + "#, + Subgraph2: r#" + type Bar @key(fields: "id") { + id: ID, + a: Int, + } + "#, + ); + assert_plan!( + &planner, + r#" + query foo($test: Boolean!) { + foo @include(if: $test) { + ... on Foo { + id + foo { + ... on Foo @include(if: $test) { + bar { + id + } + } + } + } + } + } + "#, + @r###" + QueryPlan { + Include(if: $test) { + Fetch(service: "Subgraph1") { + { + foo { + id + foo { + ... on Foo { + bar { + id + } + } + } + } + } + }, + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql b/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql new file mode 100644 index 0000000000..a6d3125d50 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql @@ -0,0 +1,65 @@ +# Composed from subgraphs with hash: d5699e8f9c56867b4a27e8b7b2e942a65af9c97b +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type Bar + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID + a: Int @join__field(graph: SUBGRAPH2) +} + +type Foo + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID + foo: Foo + bar: Bar +} + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + foo: Foo @join__field(graph: SUBGRAPH1) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql b/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql new file mode 100644 index 0000000000..bed3adbc67 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql @@ -0,0 +1,64 @@ +# Composed from subgraphs with hash: 07faf0f54a80eb7ef3e78f4fbe6382e135f6aac6 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type Bar + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID + a: Int @join__field(graph: SUBGRAPH2) +} + +type Foo + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID + bar: Bar +} + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + foo: Foo @join__field(graph: SUBGRAPH1) +} From 0d00f91959129d5dd9279105e35ba34d00ff5fdb Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 6 Sep 2024 15:24:31 +0200 Subject: [PATCH 07/56] Entity cache: make the SCAN count configurable (#5943) --- ...configuration__tests__schema_generation.snap | 7 +++++++ apollo-router/src/plugins/cache/entity.rs | 13 +++++++++++-- apollo-router/src/plugins/cache/invalidation.rs | 17 +++++++++++------ .../src/plugins/cache/invalidation_endpoint.rs | 7 +++++++ 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 2a143d67b6..058f2e3278 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -3604,6 +3604,13 @@ expression: "&schema" "path": { "description": "Specify on which path you want to listen for invalidation endpoint.", "type": "string" + }, + "scan_count": { + "default": 1000, + "description": "Number of keys to return at once from a redis SCAN command", + "format": "uint32", + "minimum": 0.0, + "type": "integer" } }, "required": [ diff --git a/apollo-router/src/plugins/cache/entity.rs b/apollo-router/src/plugins/cache/entity.rs index 3992dbd670..9c3dc0f8de 100644 --- a/apollo-router/src/plugins/cache/entity.rs +++ b/apollo-router/src/plugins/cache/entity.rs @@ -278,7 +278,15 @@ impl Plugin for EntityCache { subgraphs: subgraph_storages, }); - let invalidation = Invalidation::new(storage.clone()).await?; + let invalidation = Invalidation::new( + storage.clone(), + init.config + .invalidation + .as_ref() + .map(|i| i.scan_count) + .unwrap_or(1000), + ) + .await?; Ok(Self { storage, @@ -448,7 +456,7 @@ impl EntityCache { all: Some(storage), subgraphs: HashMap::new(), }); - let invalidation = Invalidation::new(storage.clone()).await?; + let invalidation = Invalidation::new(storage.clone(), 1000).await?; Ok(Self { storage, @@ -466,6 +474,7 @@ impl EntityCache { IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 4000, )), + scan_count: 1000, })), invalidation, }) diff --git a/apollo-router/src/plugins/cache/invalidation.rs b/apollo-router/src/plugins/cache/invalidation.rs index 2f0b71862b..2533f26bfd 100644 --- a/apollo-router/src/plugins/cache/invalidation.rs +++ b/apollo-router/src/plugins/cache/invalidation.rs @@ -67,11 +67,14 @@ pub(crate) enum InvalidationOrigin { } impl Invalidation { - pub(crate) async fn new(storage: Arc) -> Result { + pub(crate) async fn new( + storage: Arc, + scan_count: u32, + ) -> Result { let (tx, rx) = tokio::sync::mpsc::channel(CHANNEL_SIZE); tokio::task::spawn(async move { - start(storage, rx).await; + start(storage, scan_count, rx).await; }); Ok(Self { handle: tx }) } @@ -106,6 +109,7 @@ impl Invalidation { #[allow(clippy::type_complexity)] async fn start( storage: Arc, + scan_count: u32, mut handle: tokio::sync::mpsc::Receiver<( Vec, InvalidationOrigin, @@ -125,7 +129,7 @@ async fn start( ); if let Err(err) = response_tx.send( - handle_request_batch(&storage, origin, requests) + handle_request_batch(&storage, scan_count, origin, requests) .instrument(tracing::info_span!( "cache.invalidation.batch", "origin" = origin @@ -139,6 +143,7 @@ async fn start( async fn handle_request( storage: &RedisCacheStorage, + scan_count: u32, origin: &'static str, request: &InvalidationRequest, ) -> Result { @@ -149,8 +154,7 @@ async fn handle_request( key_prefix ); - // FIXME: configurable batch size - let mut stream = storage.scan(key_prefix.clone(), Some(100)); + let mut stream = storage.scan(key_prefix.clone(), Some(scan_count)); let mut count = 0u64; let mut error = None; @@ -203,6 +207,7 @@ async fn handle_request( async fn handle_request_batch( storage: &EntityStorage, + scan_count: u32, origin: &'static str, requests: Vec, ) -> Result { @@ -214,7 +219,7 @@ async fn handle_request_batch( Some(s) => s, None => continue, }; - match handle_request(redis_storage, origin, &request) + match handle_request(redis_storage, scan_count, origin, &request) .instrument(tracing::info_span!("cache.invalidation.request")) .await { diff --git a/apollo-router/src/plugins/cache/invalidation_endpoint.rs b/apollo-router/src/plugins/cache/invalidation_endpoint.rs index 18af7763f1..0ef5b7ed8d 100644 --- a/apollo-router/src/plugins/cache/invalidation_endpoint.rs +++ b/apollo-router/src/plugins/cache/invalidation_endpoint.rs @@ -39,6 +39,13 @@ pub(crate) struct InvalidationEndpointConfig { pub(crate) path: String, /// Listen address on which the invalidation endpoint must listen. pub(crate) listen: ListenAddr, + #[serde(default = "default_scan_count")] + /// Number of keys to return at once from a redis SCAN command + pub(crate) scan_count: u32, +} + +fn default_scan_count() -> u32 { + 1000 } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] From 46025d5b8e2841e1048886f9d1a2c2e638ab0f4f Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 6 Sep 2024 16:21:57 +0200 Subject: [PATCH 08/56] implement Redis connection pooling (#5942) --- ...geal_implement_redis_connection_pooling.md | 5 + apollo-router/src/cache/redis.rs | 132 +++++++++--------- apollo-router/src/configuration/mod.rs | 17 +++ ...nfiguration__tests__schema_generation.snap | 14 ++ apollo-router/tests/integration/redis.rs | 3 +- .../configuration/distributed-caching.mdx | 7 +- 6 files changed, 107 insertions(+), 71 deletions(-) create mode 100644 .changesets/feat_geal_implement_redis_connection_pooling.md diff --git a/.changesets/feat_geal_implement_redis_connection_pooling.md b/.changesets/feat_geal_implement_redis_connection_pooling.md new file mode 100644 index 0000000000..ae0dff61d1 --- /dev/null +++ b/.changesets/feat_geal_implement_redis_connection_pooling.md @@ -0,0 +1,5 @@ +### Support Redis connection pooling ([PR #5942](https://github.com/apollographql/router/pull/5942)) + +This implements Redis connection pooling, for APQ, query planner and entity cache Redis usage. This can improve performance when there is some contention on the Redis connection, or some latency in Redis calls. + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/5942 \ No newline at end of file diff --git a/apollo-router/src/cache/redis.rs b/apollo-router/src/cache/redis.rs index f16130c116..c8d7b10d07 100644 --- a/apollo-router/src/cache/redis.rs +++ b/apollo-router/src/cache/redis.rs @@ -12,6 +12,7 @@ use fred::prelude::KeysInterface; use fred::prelude::RedisClient; use fred::prelude::RedisError; use fred::prelude::RedisErrorKind; +use fred::prelude::RedisPool; use fred::types::ClusterRouting; use fred::types::Expiration; use fred::types::FromRedis; @@ -52,7 +53,7 @@ where #[derive(Clone)] pub(crate) struct RedisCacheStorage { - inner: Arc, + inner: Arc, namespace: Option>, pub(crate) ttl: Option, is_cluster: bool, @@ -168,47 +169,16 @@ impl RedisCacheStorage { }); } - let client = RedisClient::new( + Self::create_client( client_config, - Some(PerformanceConfig { - default_command_timeout: config.timeout.unwrap_or(Duration::from_millis(500)), - ..Default::default() - }), - None, - Some(ReconnectPolicy::new_exponential(0, 1, 2000, 5)), - ); - let _handle = client.connect(); - - // spawn tasks that listen for connection close or reconnect events - let mut error_rx = client.error_rx(); - let mut reconnect_rx = client.reconnect_rx(); - - tokio::spawn(async move { - while let Ok(error) = error_rx.recv().await { - tracing::error!("Client disconnected with error: {:?}", error); - } - }); - tokio::spawn(async move { - while reconnect_rx.recv().await.is_ok() { - tracing::info!("Redis client reconnected."); - } - }); - - // a TLS connection to a TCP Redis could hang, so we add a timeout - tokio::time::timeout(Duration::from_secs(5), client.wait_for_connect()) - .await - .map_err(|_| { - RedisError::new(RedisErrorKind::Timeout, "timeout connecting to Redis") - })??; - - tracing::trace!("redis connection established"); - Ok(Self { - inner: Arc::new(client), - namespace: config.namespace.map(Arc::new), - ttl: config.ttl, + config.timeout.unwrap_or(Duration::from_millis(500)), + config.pool_size as usize, + config.namespace, + config.ttl, + config.reset_ttl, is_cluster, - reset_ttl: config.reset_ttl, - }) + ) + .await } #[cfg(test)] @@ -218,34 +188,58 @@ impl RedisCacheStorage { ..Default::default() }; - let client = RedisClient::new( + Self::create_client( + client_config, + Duration::from_millis(2), + 1, + None, + None, + false, + false, + ) + .await + } + + async fn create_client( + client_config: RedisConfig, + timeout: Duration, + pool_size: usize, + namespace: Option, + ttl: Option, + reset_ttl: bool, + is_cluster: bool, + ) -> Result { + let pooled_client = RedisPool::new( client_config, Some(PerformanceConfig { - default_command_timeout: Duration::from_millis(2), + default_command_timeout: timeout, ..Default::default() }), None, Some(ReconnectPolicy::new_exponential(0, 1, 2000, 5)), - ); - let _handle = client.connect(); - - // spawn tasks that listen for connection close or reconnect events - let mut error_rx = client.error_rx(); - let mut reconnect_rx = client.reconnect_rx(); - - tokio::spawn(async move { - while let Ok(error) = error_rx.recv().await { - tracing::error!("Client disconnected with error: {:?}", error); - } - }); - tokio::spawn(async move { - while reconnect_rx.recv().await.is_ok() { - tracing::info!("Redis client reconnected."); - } - }); + pool_size, + )?; + let _handle = pooled_client.connect(); + + for client in pooled_client.clients() { + // spawn tasks that listen for connection close or reconnect events + let mut error_rx = client.error_rx(); + let mut reconnect_rx = client.reconnect_rx(); + + tokio::spawn(async move { + while let Ok(error) = error_rx.recv().await { + tracing::error!("Client disconnected with error: {:?}", error); + } + }); + tokio::spawn(async move { + while reconnect_rx.recv().await.is_ok() { + tracing::info!("Redis client reconnected."); + } + }); + } // a TLS connection to a TCP Redis could hang, so we add a timeout - tokio::time::timeout(Duration::from_secs(5), client.wait_for_connect()) + tokio::time::timeout(Duration::from_secs(5), pooled_client.wait_for_connect()) .await .map_err(|_| { RedisError::new(RedisErrorKind::Timeout, "timeout connecting to Redis") @@ -253,11 +247,11 @@ impl RedisCacheStorage { tracing::trace!("redis connection established"); Ok(Self { - inner: Arc::new(client), - ttl: None, - namespace: None, - is_cluster: false, - reset_ttl: false, + inner: Arc::new(pooled_client), + namespace: namespace.map(Arc::new), + ttl, + is_cluster, + reset_ttl, }) } @@ -370,7 +364,7 @@ impl RedisCacheStorage { key: RedisKey, ) -> Option> { if self.reset_ttl && self.ttl.is_some() { - let pipeline: fred::clients::Pipeline = self.inner.pipeline(); + let pipeline: fred::clients::Pipeline = self.inner.next().pipeline(); let key = self.make_key(key); let res = pipeline .get::(&key) @@ -541,7 +535,7 @@ impl RedisCacheStorage { None => self.inner.mset(data.to_owned()).await, Some(ttl) => { let expiration = Some(Expiration::EX(ttl.as_secs() as i64)); - let pipeline = self.inner.pipeline(); + let pipeline = self.inner.next().pipeline(); for (key, value) in data { let _ = pipeline @@ -593,9 +587,9 @@ impl RedisCacheStorage { count: Option, ) -> Pin> + Send>> { if self.is_cluster { - Box::pin(self.inner.scan_cluster(pattern, count, None)) + Box::pin(self.inner.next().scan_cluster(pattern, count, None)) } else { - Box::pin(self.inner.scan(pattern, count, None)) + Box::pin(self.inner.next().scan(pattern, count, None)) } } } diff --git a/apollo-router/src/configuration/mod.rs b/apollo-router/src/configuration/mod.rs index 8f467b9be5..6fc59167d0 100644 --- a/apollo-router/src/configuration/mod.rs +++ b/apollo-router/src/configuration/mod.rs @@ -1010,6 +1010,10 @@ pub(crate) struct QueryPlanRedisCache { #[serde(default = "default_reset_ttl")] /// When a TTL is set on a key, reset it when reading the data from that key pub(crate) reset_ttl: bool, + + #[serde(default = "default_query_planner_cache_pool_size")] + /// The size of the Redis connection pool + pub(crate) pool_size: u32, } fn default_query_plan_cache_ttl() -> Duration { @@ -1017,6 +1021,10 @@ fn default_query_plan_cache_ttl() -> Duration { Duration::from_secs(86400 * 30) } +fn default_query_planner_cache_pool_size() -> u32 { + 1 +} + /// Cache configuration #[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields, default)] @@ -1088,12 +1096,20 @@ pub(crate) struct RedisCache { #[serde(default = "default_reset_ttl")] /// When a TTL is set on a key, reset it when reading the data from that key pub(crate) reset_ttl: bool, + + #[serde(default = "default_pool_size")] + /// The size of the Redis connection pool + pub(crate) pool_size: u32, } fn default_required_to_start() -> bool { false } +fn default_pool_size() -> u32 { + 1 +} + impl From for RedisCache { fn from(value: QueryPlanRedisCache) -> Self { RedisCache { @@ -1106,6 +1122,7 @@ impl From for RedisCache { tls: value.tls, required_to_start: value.required_to_start, reset_ttl: value.reset_ttl, + pool_size: value.pool_size, } } } diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 058f2e3278..6ad7bc8e91 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -4340,6 +4340,13 @@ expression: "&schema" "nullable": true, "type": "string" }, + "pool_size": { + "default": 1, + "description": "The size of the Redis connection pool", + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, "required_to_start": { "default": false, "description": "Prevents the router from starting if it cannot connect to Redis", @@ -4550,6 +4557,13 @@ expression: "&schema" "nullable": true, "type": "string" }, + "pool_size": { + "default": 1, + "description": "The size of the Redis connection pool", + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, "required_to_start": { "default": false, "description": "Prevents the router from starting if it cannot connect to Redis", diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index f95ba37c65..16724575aa 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -369,7 +369,8 @@ async fn entity_cache() -> Result<(), BoxError> { "enabled": false, "redis": { "urls": ["redis://127.0.0.1:6379"], - "ttl": "2s" + "ttl": "2s", + "pool_size": 3 }, }, "subgraphs": { diff --git a/docs/source/configuration/distributed-caching.mdx b/docs/source/configuration/distributed-caching.mdx index e1c365e0a3..fca1123ae1 100644 --- a/docs/source/configuration/distributed-caching.mdx +++ b/docs/source/configuration/distributed-caching.mdx @@ -143,6 +143,7 @@ supergraph: #tls: required_to_start: false # Optional, defaults to false reset_ttl: true # Optional, defaults to true + pool_size: 4 # Optional, defaults to 1 ``` #### Timeout @@ -168,4 +169,8 @@ When active, the `required_to_start` option will prevent the router from startin ### Reset TTL -When this option is active, accessing a cache entry in Redis will reset its expiration. \ No newline at end of file +When this option is active, accessing a cache entry in Redis will reset its expiration. + +### Pool size + +The `pool_size` option defines the number of connections to Redis that the router will open. By default, the router will open a single connection to Redis. If there is a lot of traffic between router and Redis and/or there is some latency in thos requests, it is recommended to increase the pool size to reduce that latency. \ No newline at end of file From 5d117998045f9d0a3d24af87b8df005254f30fae Mon Sep 17 00:00:00 2001 From: Coenen Benjamin Date: Fri, 6 Sep 2024 17:25:20 +0200 Subject: [PATCH 09/56] Add ability to alias standard attributes for telemetry (#5957) Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com> Signed-off-by: Benjamin Coenen <5719034+bnjjj@users.noreply.github.com> --- .changesets/feat_bnjjj_fix_5930.md | 20 + .../0027.entity_cache_type_metrics.yaml | 5 + ...nfiguration__tests__schema_generation.snap | 800 ++++++++---------- .../telemetry/config_new/attributes.rs | 510 +++++++---- .../telemetry/config_new/cache/attributes.rs | 8 +- .../plugins/telemetry/config_new/cache/mod.rs | 18 +- .../plugins/telemetry/config_new/cost/mod.rs | 41 +- .../telemetry/config_new/extendable.rs | 11 +- .../metrics.snap | 4 +- .../router.yaml | 2 + .../fixtures/subgraph/caching/metrics.snap | 11 +- .../fixtures/subgraph/caching/router.yaml | 3 +- .../metrics.snap | 23 + .../router.yaml | 9 + .../custom_histogram_duration copy/test.yaml | 30 + .../config_new/graphql/attributes.rs | 85 +- .../src/plugins/telemetry/config_new/spans.rs | 42 + docs/source/configuration/entity-caching.mdx | 2 +- .../instrumentation/standard-attributes.mdx | 15 + 19 files changed, 943 insertions(+), 696 deletions(-) create mode 100644 .changesets/feat_bnjjj_fix_5930.md create mode 100644 apollo-router/src/configuration/migrations/0027.entity_cache_type_metrics.yaml create mode 100644 apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/metrics.snap create mode 100644 apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/router.yaml create mode 100644 apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/test.yaml diff --git a/.changesets/feat_bnjjj_fix_5930.md b/.changesets/feat_bnjjj_fix_5930.md new file mode 100644 index 0000000000..f8c2ac78f6 --- /dev/null +++ b/.changesets/feat_bnjjj_fix_5930.md @@ -0,0 +1,20 @@ +### Add ability to alias standard attributes for telemetry ([Issue #5930](https://github.com/apollographql/router/issues/5930)) + +There is an issue when using standard attributes (on cache for example) because on new relic `entity.type` is a reserved attribute name and so it won’t work properly. cf [Learn about New Relic entities](https://docs.newrelic.com/docs/new-relic-solutions/new-relic-one/core-concepts/what-entity-new-relic/#reserved-attributes) Moreover `entity.type` is not consistent with our other graphql attributes (prefixed by `graphql.`). So we rename `entity.type` attribute to `graphql.type.name`. + +In order to make it work and that could also answer other use cases that would be great if we can alias the name of a standard attribute like this: + +```yaml +telemetry: + instrumentation: + spans: + mode: spec_compliant # Docs state this significantly improves performance: https://www.apollographql.com/docs/router/configuration/telemetry/instrumentation/spans#spec_compliant + instruments: + cache: # Cache instruments configuration + apollo.router.operations.entity.cache: # A counter which counts the number of cache hit and miss for subgraph requests + attributes: + graphql.type.name: + alias: entity_type # ENABLED and aliased to entity_type +``` + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/5957 diff --git a/apollo-router/src/configuration/migrations/0027.entity_cache_type_metrics.yaml b/apollo-router/src/configuration/migrations/0027.entity_cache_type_metrics.yaml new file mode 100644 index 0000000000..e55c9ea6d4 --- /dev/null +++ b/apollo-router/src/configuration/migrations/0027.entity_cache_type_metrics.yaml @@ -0,0 +1,5 @@ +description: Entity cache entity.type attribute renamed to graphql.type.name +actions: + - type: move + from: telemetry.instrumentation.instruments.cache["apollo.router.operations.entity.cache"].attributes["entity.type"] + to: telemetry.instrumentation.instruments.cache["apollo.router.operations.entity.cache"].attributes["graphql.type.name"] \ No newline at end of file diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 6ad7bc8e91..c514cb925d 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -470,11 +470,10 @@ expression: "&schema" "CacheAttributes": { "additionalProperties": false, "properties": { - "entity.type": { - "default": null, - "description": "Entity type", - "nullable": true, - "type": "boolean" + "graphql.type.name": { + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -2787,34 +2786,29 @@ expression: "&schema" "additionalProperties": false, "properties": { "graphql.field.name": { - "default": null, - "description": "The GraphQL field name", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.field.type": { - "default": null, - "description": "The GraphQL field type", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.list.length": { - "default": null, - "description": "If the field is a list, the length of the list", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.name": { - "default": null, - "description": "The GraphQL operation name", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.type.name": { - "default": null, - "description": "The GraphQL type name", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -4730,136 +4724,114 @@ expression: "&schema" "type": "boolean" }, "dd.trace_id": { - "default": null, - "description": "The datadog trace ID. This can be output in logs and used to correlate traces in Datadog.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "error.type": { - "default": null, - "description": "Describes a class of error the operation ended with. Examples:\n\n* timeout * name_resolution_error * 500\n\nRequirement level: Conditionally Required: If request has ended with an error.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.request.body.size": { - "default": null, - "description": "The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples:\n\n* 3495\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.request.method": { - "default": null, - "description": "HTTP request method. Examples:\n\n* GET * POST * HEAD\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.response.body.size": { - "default": null, - "description": "The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples:\n\n* 3495\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.response.status_code": { - "default": null, - "description": "HTTP response status code. Examples:\n\n* 200\n\nRequirement level: Conditionally Required: If and only if one was received/sent.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.route": { - "default": null, - "description": "The matched route (path template in the format used by the respective server framework). Examples:\n\n* /graphql\n\nRequirement level: Conditionally Required: If and only if it’s available", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.local.address": { - "default": null, - "description": "Local socket address. Useful in case of a multi-IP host. Examples:\n\n* 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Opt-In", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.local.port": { - "default": null, - "description": "Local socket port. Useful in case of a multi-port host. Examples:\n\n* 65123\n\nRequirement level: Opt-In", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.peer.address": { - "default": null, - "description": "Peer address of the network connection - IP address or Unix domain socket name. Examples:\n\n* 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.peer.port": { - "default": null, - "description": "Peer port number of the network connection. Examples:\n\n* 65123\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.protocol.name": { - "default": null, - "description": "OSI application layer or non-OSI equivalent. Examples:\n\n* http * spdy\n\nRequirement level: Recommended: if not default (http).", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.protocol.version": { - "default": null, - "description": "Version of the protocol specified in network.protocol.name. Examples:\n\n* 1.0 * 1.1 * 2 * 3\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.transport": { - "default": null, - "description": "OSI transport layer. Examples:\n\n* tcp * udp\n\nRequirement level: Conditionally Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.type": { - "default": null, - "description": "OSI network layer or non-OSI equivalent. Examples:\n\n* ipv4 * ipv6\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "server.address": { - "default": null, - "description": "Name of the local HTTP server that received the request. Examples:\n\n* example.com * 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "server.port": { - "default": null, - "description": "Port of the local HTTP server that received the request. Examples:\n\n* 80 * 8080 * 443\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "trace_id": { - "default": null, - "description": "The OpenTelemetry trace ID. This can be output in logs.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.path": { - "default": null, - "description": "The URI path component Examples:\n\n* /search\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.query": { - "default": null, - "description": "The URI query component Examples:\n\n* q=OpenTelemetry\n\nRequirement level: Conditionally Required: If and only if one was received/sent.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.scheme": { - "default": null, - "description": "The URI scheme component identifying the used protocol. Examples:\n\n* http * https\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "user_agent.original": { - "default": null, - "description": "Value of the HTTP User-Agent header sent by the client. Examples:\n\n* CERN-LineMode/2.15 * libwww/2.17b3\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -5459,6 +5431,25 @@ expression: "&schema" ], "type": "string" }, + "StandardAttribute": { + "anyOf": [ + { + "type": "boolean" + }, + { + "additionalProperties": false, + "properties": { + "alias": { + "type": "string" + } + }, + "required": [ + "alias" + ], + "type": "object" + } + ] + }, "StandardEventConfig_for_RouterSelector": { "anyOf": [ { @@ -5676,28 +5667,24 @@ expression: "&schema" "additionalProperties": false, "properties": { "subgraph.graphql.document": { - "default": null, - "description": "The GraphQL document being executed. Examples:\n\n* `query findBookById { bookById(id: ?) { name } }`\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.graphql.operation.name": { - "default": null, - "description": "The name of the operation being executed. Examples:\n\n* findBookById\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.graphql.operation.type": { - "default": null, - "description": "The type of the operation being executed. Examples:\n\n* query * subscription * mutation\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.name": { - "default": null, - "description": "The name of the subgraph Examples:\n\n* products\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -6577,46 +6564,39 @@ expression: "&schema" "description": "Attributes for Cost", "properties": { "cost.actual": { - "default": null, - "description": "The actual cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.delta": { - "default": null, - "description": "The delta (estimated - actual) cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.estimated": { - "default": null, - "description": "The estimated cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.result": { - "default": null, - "description": "The cost result, this is an error code returned by the cost calculation or COST_OK", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.document": { - "default": null, - "description": "The GraphQL document being executed. Examples:\n\n* `query findBookById { bookById(id: ?) { name } }`\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.name": { - "default": null, - "description": "The name of the operation being executed. Examples:\n\n* findBookById\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.type": { - "default": null, - "description": "The type of the operation being executed. Examples:\n\n* query * subscription * mutation\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -7439,136 +7419,114 @@ expression: "&schema" "type": "boolean" }, "dd.trace_id": { - "default": null, - "description": "The datadog trace ID. This can be output in logs and used to correlate traces in Datadog.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "error.type": { - "default": null, - "description": "Describes a class of error the operation ended with. Examples:\n\n* timeout * name_resolution_error * 500\n\nRequirement level: Conditionally Required: If request has ended with an error.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.request.body.size": { - "default": null, - "description": "The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples:\n\n* 3495\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.request.method": { - "default": null, - "description": "HTTP request method. Examples:\n\n* GET * POST * HEAD\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.response.body.size": { - "default": null, - "description": "The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples:\n\n* 3495\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.response.status_code": { - "default": null, - "description": "HTTP response status code. Examples:\n\n* 200\n\nRequirement level: Conditionally Required: If and only if one was received/sent.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.route": { - "default": null, - "description": "The matched route (path template in the format used by the respective server framework). Examples:\n\n* /graphql\n\nRequirement level: Conditionally Required: If and only if it’s available", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.local.address": { - "default": null, - "description": "Local socket address. Useful in case of a multi-IP host. Examples:\n\n* 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Opt-In", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.local.port": { - "default": null, - "description": "Local socket port. Useful in case of a multi-port host. Examples:\n\n* 65123\n\nRequirement level: Opt-In", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.peer.address": { - "default": null, - "description": "Peer address of the network connection - IP address or Unix domain socket name. Examples:\n\n* 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.peer.port": { - "default": null, - "description": "Peer port number of the network connection. Examples:\n\n* 65123\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.protocol.name": { - "default": null, - "description": "OSI application layer or non-OSI equivalent. Examples:\n\n* http * spdy\n\nRequirement level: Recommended: if not default (http).", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.protocol.version": { - "default": null, - "description": "Version of the protocol specified in network.protocol.name. Examples:\n\n* 1.0 * 1.1 * 2 * 3\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.transport": { - "default": null, - "description": "OSI transport layer. Examples:\n\n* tcp * udp\n\nRequirement level: Conditionally Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.type": { - "default": null, - "description": "OSI network layer or non-OSI equivalent. Examples:\n\n* ipv4 * ipv6\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "server.address": { - "default": null, - "description": "Name of the local HTTP server that received the request. Examples:\n\n* example.com * 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "server.port": { - "default": null, - "description": "Port of the local HTTP server that received the request. Examples:\n\n* 80 * 8080 * 443\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "trace_id": { - "default": null, - "description": "The OpenTelemetry trace ID. This can be output in logs.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.path": { - "default": null, - "description": "The URI path component Examples:\n\n* /search\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.query": { - "default": null, - "description": "The URI query component Examples:\n\n* q=OpenTelemetry\n\nRequirement level: Conditionally Required: If and only if one was received/sent.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.scheme": { - "default": null, - "description": "The URI scheme component identifying the used protocol. Examples:\n\n* http * https\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "user_agent.original": { - "default": null, - "description": "Value of the HTTP User-Agent header sent by the client. Examples:\n\n* CERN-LineMode/2.15 * libwww/2.17b3\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -7587,136 +7545,114 @@ expression: "&schema" "type": "boolean" }, "dd.trace_id": { - "default": null, - "description": "The datadog trace ID. This can be output in logs and used to correlate traces in Datadog.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "error.type": { - "default": null, - "description": "Describes a class of error the operation ended with. Examples:\n\n* timeout * name_resolution_error * 500\n\nRequirement level: Conditionally Required: If request has ended with an error.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.request.body.size": { - "default": null, - "description": "The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples:\n\n* 3495\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.request.method": { - "default": null, - "description": "HTTP request method. Examples:\n\n* GET * POST * HEAD\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.response.body.size": { - "default": null, - "description": "The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples:\n\n* 3495\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.response.status_code": { - "default": null, - "description": "HTTP response status code. Examples:\n\n* 200\n\nRequirement level: Conditionally Required: If and only if one was received/sent.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.route": { - "default": null, - "description": "The matched route (path template in the format used by the respective server framework). Examples:\n\n* /graphql\n\nRequirement level: Conditionally Required: If and only if it’s available", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.local.address": { - "default": null, - "description": "Local socket address. Useful in case of a multi-IP host. Examples:\n\n* 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Opt-In", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.local.port": { - "default": null, - "description": "Local socket port. Useful in case of a multi-port host. Examples:\n\n* 65123\n\nRequirement level: Opt-In", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.peer.address": { - "default": null, - "description": "Peer address of the network connection - IP address or Unix domain socket name. Examples:\n\n* 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.peer.port": { - "default": null, - "description": "Peer port number of the network connection. Examples:\n\n* 65123\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.protocol.name": { - "default": null, - "description": "OSI application layer or non-OSI equivalent. Examples:\n\n* http * spdy\n\nRequirement level: Recommended: if not default (http).", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.protocol.version": { - "default": null, - "description": "Version of the protocol specified in network.protocol.name. Examples:\n\n* 1.0 * 1.1 * 2 * 3\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.transport": { - "default": null, - "description": "OSI transport layer. Examples:\n\n* tcp * udp\n\nRequirement level: Conditionally Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.type": { - "default": null, - "description": "OSI network layer or non-OSI equivalent. Examples:\n\n* ipv4 * ipv6\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "server.address": { - "default": null, - "description": "Name of the local HTTP server that received the request. Examples:\n\n* example.com * 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "server.port": { - "default": null, - "description": "Port of the local HTTP server that received the request. Examples:\n\n* 80 * 8080 * 443\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "trace_id": { - "default": null, - "description": "The OpenTelemetry trace ID. This can be output in logs.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.path": { - "default": null, - "description": "The URI path component Examples:\n\n* /search\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.query": { - "default": null, - "description": "The URI query component Examples:\n\n* q=OpenTelemetry\n\nRequirement level: Conditionally Required: If and only if one was received/sent.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.scheme": { - "default": null, - "description": "The URI scheme component identifying the used protocol. Examples:\n\n* http * https\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "user_agent.original": { - "default": null, - "description": "Value of the HTTP User-Agent header sent by the client. Examples:\n\n* CERN-LineMode/2.15 * libwww/2.17b3\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -7728,28 +7664,24 @@ expression: "&schema" }, "properties": { "subgraph.graphql.document": { - "default": null, - "description": "The GraphQL document being executed. Examples:\n\n* `query findBookById { bookById(id: ?) { name } }`\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.graphql.operation.name": { - "default": null, - "description": "The name of the operation being executed. Examples:\n\n* findBookById\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.graphql.operation.type": { - "default": null, - "description": "The type of the operation being executed. Examples:\n\n* query * subscription * mutation\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.name": { - "default": null, - "description": "The name of the subgraph Examples:\n\n* products\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -7761,28 +7693,24 @@ expression: "&schema" }, "properties": { "subgraph.graphql.document": { - "default": null, - "description": "The GraphQL document being executed. Examples:\n\n* `query findBookById { bookById(id: ?) { name } }`\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.graphql.operation.name": { - "default": null, - "description": "The name of the operation being executed. Examples:\n\n* findBookById\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.graphql.operation.type": { - "default": null, - "description": "The type of the operation being executed. Examples:\n\n* query * subscription * mutation\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.name": { - "default": null, - "description": "The name of the subgraph Examples:\n\n* products\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -7795,46 +7723,39 @@ expression: "&schema" "description": "Attributes for Cost", "properties": { "cost.actual": { - "default": null, - "description": "The actual cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.delta": { - "default": null, - "description": "The delta (estimated - actual) cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.estimated": { - "default": null, - "description": "The estimated cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.result": { - "default": null, - "description": "The cost result, this is an error code returned by the cost calculation or COST_OK", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.document": { - "default": null, - "description": "The GraphQL document being executed. Examples:\n\n* `query findBookById { bookById(id: ?) { name } }`\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.name": { - "default": null, - "description": "The name of the operation being executed. Examples:\n\n* findBookById\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.type": { - "default": null, - "description": "The type of the operation being executed. Examples:\n\n* query * subscription * mutation\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -7847,46 +7768,39 @@ expression: "&schema" "description": "Attributes for Cost", "properties": { "cost.actual": { - "default": null, - "description": "The actual cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.delta": { - "default": null, - "description": "The delta (estimated - actual) cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.estimated": { - "default": null, - "description": "The estimated cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.result": { - "default": null, - "description": "The cost result, this is an error code returned by the cost calculation or COST_OK", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.document": { - "default": null, - "description": "The GraphQL document being executed. Examples:\n\n* `query findBookById { bookById(id: ?) { name } }`\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.name": { - "default": null, - "description": "The name of the operation being executed. Examples:\n\n* findBookById\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.type": { - "default": null, - "description": "The type of the operation being executed. Examples:\n\n* query * subscription * mutation\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -7910,11 +7824,10 @@ expression: "&schema" "description": "#/definitions/SubgraphSelector" }, "properties": { - "entity.type": { - "default": null, - "description": "Entity type", - "nullable": true, - "type": "boolean" + "graphql.type.name": { + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -8006,34 +7919,29 @@ expression: "&schema" }, "properties": { "graphql.field.name": { - "default": null, - "description": "The GraphQL field name", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.field.type": { - "default": null, - "description": "The GraphQL field type", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.list.length": { - "default": null, - "description": "If the field is a list, the length of the list", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.name": { - "default": null, - "description": "The GraphQL operation name", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.type.name": { - "default": null, - "description": "The GraphQL type name", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" diff --git a/apollo-router/src/plugins/telemetry/config_new/attributes.rs b/apollo-router/src/plugins/telemetry/config_new/attributes.rs index 5ce016cc0e..53640cd87b 100644 --- a/apollo-router/src/plugins/telemetry/config_new/attributes.rs +++ b/apollo-router/src/plugins/telemetry/config_new/attributes.rs @@ -77,6 +77,23 @@ pub(crate) enum DefaultAttributeRequirementLevel { Recommended, } +#[derive(Deserialize, JsonSchema, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields, rename_all = "snake_case", untagged)] +pub(crate) enum StandardAttribute { + Bool(bool), + Aliased { alias: String }, +} + +impl StandardAttribute { + pub(crate) fn key(&self, original_key: Key) -> Option { + match self { + StandardAttribute::Bool(true) => Some(original_key), + StandardAttribute::Aliased { alias } => Some(Key::new(alias.clone())), + _ => None, + } + } +} + #[derive(Deserialize, JsonSchema, Clone, Default, Debug)] #[cfg_attr(test, derive(PartialEq))] #[serde(deny_unknown_fields, default)] @@ -84,12 +101,11 @@ pub(crate) struct RouterAttributes { /// The datadog trace ID. /// This can be output in logs and used to correlate traces in Datadog. #[serde(rename = "dd.trace_id")] - pub(crate) datadog_trace_id: Option, + pub(crate) datadog_trace_id: Option, /// The OpenTelemetry trace ID. /// This can be output in logs. - #[serde(rename = "trace_id")] - pub(crate) trace_id: Option, + pub(crate) trace_id: Option, /// All key values from trace baggage. pub(crate) baggage: Option, @@ -124,7 +140,7 @@ pub(crate) struct SupergraphAttributes { /// /// Requirement level: Recommended #[serde(rename = "graphql.document")] - pub(crate) graphql_document: Option, + pub(crate) graphql_document: Option, /// The name of the operation being executed. /// Examples: @@ -133,7 +149,7 @@ pub(crate) struct SupergraphAttributes { /// /// Requirement level: Recommended #[serde(rename = "graphql.operation.name")] - pub(crate) graphql_operation_name: Option, + pub(crate) graphql_operation_name: Option, /// The type of the operation being executed. /// Examples: @@ -144,7 +160,7 @@ pub(crate) struct SupergraphAttributes { /// /// Requirement level: Recommended #[serde(rename = "graphql.operation.type")] - pub(crate) graphql_operation_type: Option, + pub(crate) graphql_operation_type: Option, /// Cost attributes for the operation being executed #[serde(flatten)] @@ -161,13 +177,13 @@ impl DefaultForLevel for SupergraphAttributes { DefaultAttributeRequirementLevel::Required => {} DefaultAttributeRequirementLevel::Recommended => { if self.graphql_document.is_none() { - self.graphql_document = Some(true); + self.graphql_document = Some(StandardAttribute::Bool(true)); } if self.graphql_operation_name.is_none() { - self.graphql_operation_name = Some(true); + self.graphql_operation_name = Some(StandardAttribute::Bool(true)); } if self.graphql_operation_type.is_none() { - self.graphql_operation_type = Some(true); + self.graphql_operation_type = Some(StandardAttribute::Bool(true)); } } DefaultAttributeRequirementLevel::None => {} @@ -185,7 +201,7 @@ pub(crate) struct SubgraphAttributes { /// /// Requirement level: Required #[serde(rename = "subgraph.name")] - subgraph_name: Option, + subgraph_name: Option, /// The GraphQL document being executed. /// Examples: @@ -194,7 +210,7 @@ pub(crate) struct SubgraphAttributes { /// /// Requirement level: Recommended #[serde(rename = "subgraph.graphql.document")] - graphql_document: Option, + graphql_document: Option, /// The name of the operation being executed. /// Examples: @@ -203,7 +219,7 @@ pub(crate) struct SubgraphAttributes { /// /// Requirement level: Recommended #[serde(rename = "subgraph.graphql.operation.name")] - graphql_operation_name: Option, + graphql_operation_name: Option, /// The type of the operation being executed. /// Examples: @@ -214,7 +230,7 @@ pub(crate) struct SubgraphAttributes { /// /// Requirement level: Recommended #[serde(rename = "subgraph.graphql.operation.type")] - graphql_operation_type: Option, + graphql_operation_type: Option, } impl DefaultForLevel for SubgraphAttributes { @@ -226,21 +242,21 @@ impl DefaultForLevel for SubgraphAttributes { match requirement_level { DefaultAttributeRequirementLevel::Required => { if self.subgraph_name.is_none() { - self.subgraph_name = Some(true); + self.subgraph_name = Some(StandardAttribute::Bool(true)); } } DefaultAttributeRequirementLevel::Recommended => { if self.subgraph_name.is_none() { - self.subgraph_name = Some(true); + self.subgraph_name = Some(StandardAttribute::Bool(true)); } if self.graphql_document.is_none() { - self.graphql_document = Some(true); + self.graphql_document = Some(StandardAttribute::Bool(true)); } if self.graphql_operation_name.is_none() { - self.graphql_operation_name = Some(true); + self.graphql_operation_name = Some(StandardAttribute::Bool(true)); } if self.graphql_operation_type.is_none() { - self.graphql_operation_type = Some(true); + self.graphql_operation_type = Some(StandardAttribute::Bool(true)); } } DefaultAttributeRequirementLevel::None => {} @@ -263,7 +279,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Conditionally Required: If request has ended with an error. #[serde(rename = "error.type")] - pub(crate) error_type: Option, + pub(crate) error_type: Option, /// The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. /// Examples: @@ -272,7 +288,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Recommended #[serde(rename = "http.request.body.size")] - pub(crate) http_request_body_size: Option, + pub(crate) http_request_body_size: Option, /// HTTP request method. /// Examples: @@ -283,7 +299,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Required #[serde(rename = "http.request.method")] - pub(crate) http_request_method: Option, + pub(crate) http_request_method: Option, /// Original HTTP method sent by the client in the request line. /// Examples: @@ -294,7 +310,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Conditionally Required (If and only if it’s different than http.request.method) #[serde(rename = "http.request.method.original", skip)] - pub(crate) http_request_method_original: Option, + pub(crate) http_request_method_original: Option, /// The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. /// Examples: @@ -303,7 +319,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Recommended #[serde(rename = "http.response.body.size")] - pub(crate) http_response_body_size: Option, + pub(crate) http_response_body_size: Option, /// HTTP response status code. /// Examples: @@ -312,7 +328,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Conditionally Required: If and only if one was received/sent. #[serde(rename = "http.response.status_code")] - pub(crate) http_response_status_code: Option, + pub(crate) http_response_status_code: Option, /// OSI application layer or non-OSI equivalent. /// Examples: @@ -322,7 +338,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Recommended: if not default (http). #[serde(rename = "network.protocol.name")] - pub(crate) network_protocol_name: Option, + pub(crate) network_protocol_name: Option, /// Version of the protocol specified in network.protocol.name. /// Examples: @@ -334,7 +350,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Recommended #[serde(rename = "network.protocol.version")] - pub(crate) network_protocol_version: Option, + pub(crate) network_protocol_version: Option, /// OSI transport layer. /// Examples: @@ -344,7 +360,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Conditionally Required #[serde(rename = "network.transport")] - pub(crate) network_transport: Option, + pub(crate) network_transport: Option, /// OSI network layer or non-OSI equivalent. /// Examples: @@ -354,7 +370,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Recommended #[serde(rename = "network.type")] - pub(crate) network_type: Option, + pub(crate) network_type: Option, } impl DefaultForLevel for HttpCommonAttributes { @@ -366,13 +382,13 @@ impl DefaultForLevel for HttpCommonAttributes { match requirement_level { DefaultAttributeRequirementLevel::Required => { if self.error_type.is_none() { - self.error_type = Some(true); + self.error_type = Some(StandardAttribute::Bool(true)); } if self.http_request_method.is_none() { - self.http_request_method = Some(true); + self.http_request_method = Some(StandardAttribute::Bool(true)); } if self.http_response_status_code.is_none() { - self.http_response_status_code = Some(true); + self.http_response_status_code = Some(StandardAttribute::Bool(true)); } } DefaultAttributeRequirementLevel::Recommended => { @@ -380,21 +396,21 @@ impl DefaultForLevel for HttpCommonAttributes { match kind { TelemetryDataKind::Traces => { if self.http_request_body_size.is_none() { - self.http_request_body_size = Some(true); + self.http_request_body_size = Some(StandardAttribute::Bool(true)); } if self.http_response_body_size.is_none() { - self.http_response_body_size = Some(true); + self.http_response_body_size = Some(StandardAttribute::Bool(true)); } if self.network_protocol_version.is_none() { - self.network_protocol_version = Some(true); + self.network_protocol_version = Some(StandardAttribute::Bool(true)); } if self.network_type.is_none() { - self.network_type = Some(true); + self.network_type = Some(StandardAttribute::Bool(true)); } } TelemetryDataKind::Metrics => { if self.network_protocol_version.is_none() { - self.network_protocol_version = Some(true); + self.network_protocol_version = Some(StandardAttribute::Bool(true)); } } } @@ -417,7 +433,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Recommended #[serde(rename = "client.address", skip)] - pub(crate) client_address: Option, + pub(crate) client_address: Option, /// The port of the original client behind all proxies, if known (e.g. from Forwarded or a similar header). Otherwise, the immediate client peer port. /// Examples: /// @@ -425,7 +441,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Recommended #[serde(rename = "client.port", skip)] - pub(crate) client_port: Option, + pub(crate) client_port: Option, /// The matched route (path template in the format used by the respective server framework). /// Examples: /// @@ -433,7 +449,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Conditionally Required: If and only if it’s available #[serde(rename = "http.route")] - pub(crate) http_route: Option, + pub(crate) http_route: Option, /// Local socket address. Useful in case of a multi-IP host. /// Examples: /// @@ -442,7 +458,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Opt-In #[serde(rename = "network.local.address")] - pub(crate) network_local_address: Option, + pub(crate) network_local_address: Option, /// Local socket port. Useful in case of a multi-port host. /// Examples: /// @@ -450,7 +466,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Opt-In #[serde(rename = "network.local.port")] - pub(crate) network_local_port: Option, + pub(crate) network_local_port: Option, /// Peer address of the network connection - IP address or Unix domain socket name. /// Examples: /// @@ -459,7 +475,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Recommended #[serde(rename = "network.peer.address")] - pub(crate) network_peer_address: Option, + pub(crate) network_peer_address: Option, /// Peer port number of the network connection. /// Examples: /// @@ -467,7 +483,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Recommended #[serde(rename = "network.peer.port")] - pub(crate) network_peer_port: Option, + pub(crate) network_peer_port: Option, /// Name of the local HTTP server that received the request. /// Examples: /// @@ -477,7 +493,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Recommended #[serde(rename = "server.address")] - pub(crate) server_address: Option, + pub(crate) server_address: Option, /// Port of the local HTTP server that received the request. /// Examples: /// @@ -487,7 +503,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Recommended #[serde(rename = "server.port")] - pub(crate) server_port: Option, + pub(crate) server_port: Option, /// The URI path component /// Examples: /// @@ -495,7 +511,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Required #[serde(rename = "url.path")] - pub(crate) url_path: Option, + pub(crate) url_path: Option, /// The URI query component /// Examples: /// @@ -503,7 +519,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Conditionally Required: If and only if one was received/sent. #[serde(rename = "url.query")] - pub(crate) url_query: Option, + pub(crate) url_query: Option, /// The URI scheme component identifying the used protocol. /// Examples: @@ -513,7 +529,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Required #[serde(rename = "url.scheme")] - pub(crate) url_scheme: Option, + pub(crate) url_scheme: Option, /// Value of the HTTP User-Agent header sent by the client. /// Examples: @@ -523,7 +539,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Recommended #[serde(rename = "user_agent.original")] - pub(crate) user_agent_original: Option, + pub(crate) user_agent_original: Option, } impl DefaultForLevel for HttpServerAttributes { @@ -536,41 +552,41 @@ impl DefaultForLevel for HttpServerAttributes { DefaultAttributeRequirementLevel::Required => match kind { TelemetryDataKind::Traces => { if self.url_scheme.is_none() { - self.url_scheme = Some(true); + self.url_scheme = Some(StandardAttribute::Bool(true)); } if self.url_path.is_none() { - self.url_path = Some(true); + self.url_path = Some(StandardAttribute::Bool(true)); } if self.url_query.is_none() { - self.url_query = Some(true); + self.url_query = Some(StandardAttribute::Bool(true)); } if self.http_route.is_none() { - self.http_route = Some(true); + self.http_route = Some(StandardAttribute::Bool(true)); } } TelemetryDataKind::Metrics => { if self.server_address.is_none() { - self.server_address = Some(true); + self.server_address = Some(StandardAttribute::Bool(true)); } if self.server_port.is_none() && self.server_address.is_some() { - self.server_port = Some(true); + self.server_port = Some(StandardAttribute::Bool(true)); } } }, DefaultAttributeRequirementLevel::Recommended => match kind { TelemetryDataKind::Traces => { if self.client_address.is_none() { - self.client_address = Some(true); + self.client_address = Some(StandardAttribute::Bool(true)); } if self.server_address.is_none() { - self.server_address = Some(true); + self.server_address = Some(StandardAttribute::Bool(true)); } if self.server_port.is_none() && self.server_address.is_some() { - self.server_port = Some(true); + self.server_port = Some(StandardAttribute::Bool(true)); } if self.user_agent_original.is_none() { - self.user_agent_original = Some(true); + self.user_agent_original = Some(StandardAttribute::Bool(true)); } } TelemetryDataKind::Metrics => {} @@ -588,20 +604,23 @@ impl Selectors for RouterAttributes { fn on_request(&self, request: &router::Request) -> Vec { let mut attrs = self.common.on_request(request); attrs.extend(self.server.on_request(request)); - if let Some(true) = &self.trace_id { + if let Some(key) = self + .trace_id + .as_ref() + .and_then(|a| a.key(Key::from_static_str("trace_id"))) + { if let Some(trace_id) = trace_id() { - attrs.push(KeyValue::new( - Key::from_static_str("trace_id"), - trace_id.to_string(), - )); + attrs.push(KeyValue::new(key, trace_id.to_string())); } } - if let Some(true) = &self.datadog_trace_id { + + if let Some(key) = self + .datadog_trace_id + .as_ref() + .and_then(|a| a.key(Key::from_static_str("dd.trace_id"))) + { if let Some(trace_id) = trace_id() { - attrs.push(KeyValue::new( - Key::from_static_str("dd.trace_id"), - trace_id.to_datadog(), - )); + attrs.push(KeyValue::new(key, trace_id.to_datadog())); } } if let Some(true) = &self.baggage { @@ -635,14 +654,22 @@ impl Selectors for HttpCommonAttributes { fn on_request(&self, request: &router::Request) -> Vec { let mut attrs = Vec::new(); - if let Some(true) = &self.http_request_method { + if let Some(key) = self + .http_request_method + .as_ref() + .and_then(|a| a.key(HTTP_REQUEST_METHOD)) + { attrs.push(KeyValue::new( - HTTP_REQUEST_METHOD, + key, request.router_request.method().as_str().to_string(), )); } - if let Some(true) = &self.http_request_body_size { + if let Some(key) = self + .http_request_body_size + .as_ref() + .and_then(|a| a.key(HTTP_REQUEST_BODY_SIZE)) + { if let Some(content_length) = request .router_request .headers() @@ -651,35 +678,47 @@ impl Selectors for HttpCommonAttributes { { if let Ok(content_length) = content_length.parse::() { attrs.push(KeyValue::new( - HTTP_REQUEST_BODY_SIZE, + key, opentelemetry::Value::I64(content_length), )); } } } - if let Some(true) = &self.network_protocol_name { + if let Some(key) = self + .network_protocol_name + .as_ref() + .and_then(|a| a.key(NETWORK_PROTOCOL_NAME)) + { if let Some(scheme) = request.router_request.uri().scheme() { - attrs.push(KeyValue::new(NETWORK_PROTOCOL_NAME, scheme.to_string())); + attrs.push(KeyValue::new(key, scheme.to_string())); } } - if let Some(true) = &self.network_protocol_version { + if let Some(key) = self + .network_protocol_version + .as_ref() + .and_then(|a| a.key(NETWORK_PROTOCOL_VERSION)) + { attrs.push(KeyValue::new( - NETWORK_PROTOCOL_VERSION, + key, format!("{:?}", request.router_request.version()), )); } - if let Some(true) = &self.network_transport { - attrs.push(KeyValue::new(NETWORK_TRANSPORT, "tcp".to_string())); + if let Some(key) = self + .network_transport + .as_ref() + .and_then(|a| a.key(NETWORK_TRANSPORT)) + { + attrs.push(KeyValue::new(key, "tcp".to_string())); } - if let Some(true) = &self.network_type { + if let Some(key) = self.network_type.as_ref().and_then(|a| a.key(NETWORK_TYPE)) { if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.server_address { if socket.is_ipv4() { - attrs.push(KeyValue::new(NETWORK_TYPE, "ipv4".to_string())); + attrs.push(KeyValue::new(key, "ipv4".to_string())); } else if socket.is_ipv6() { - attrs.push(KeyValue::new(NETWORK_TYPE, "ipv6".to_string())); + attrs.push(KeyValue::new(key, "ipv6".to_string())); } } } @@ -690,7 +729,11 @@ impl Selectors for HttpCommonAttributes { fn on_response(&self, response: &router::Response) -> Vec { let mut attrs = Vec::new(); - if let Some(true) = &self.http_response_body_size { + if let Some(key) = self + .http_response_body_size + .as_ref() + .and_then(|a| a.key(HTTP_RESPONSE_BODY_SIZE)) + { if let Some(content_length) = response .response .headers() @@ -699,24 +742,28 @@ impl Selectors for HttpCommonAttributes { { if let Ok(content_length) = content_length.parse::() { attrs.push(KeyValue::new( - HTTP_RESPONSE_BODY_SIZE, + key, opentelemetry::Value::I64(content_length), )); } } } - if let Some(true) = &self.http_response_status_code { + if let Some(key) = self + .http_response_status_code + .as_ref() + .and_then(|a| a.key(HTTP_RESPONSE_STATUS_CODE)) + { attrs.push(KeyValue::new( - HTTP_RESPONSE_STATUS_CODE, + key, response.response.status().as_u16() as i64, )); } - if let Some(true) = &self.error_type { + if let Some(key) = self.error_type.as_ref().and_then(|a| a.key(ERROR_TYPE)) { if !response.response.status().is_success() { attrs.push(KeyValue::new( - ERROR_TYPE, + key, response .response .status() @@ -731,17 +778,21 @@ impl Selectors for HttpCommonAttributes { fn on_error(&self, _error: &BoxError, _ctx: &Context) -> Vec { let mut attrs = Vec::new(); - if let Some(true) = &self.error_type { + if let Some(key) = self.error_type.as_ref().and_then(|a| a.key(ERROR_TYPE)) { attrs.push(KeyValue::new( - ERROR_TYPE, + key, StatusCode::INTERNAL_SERVER_ERROR .canonical_reason() .unwrap_or("unknown"), )); } - if let Some(true) = &self.http_response_status_code { + if let Some(key) = self + .http_response_status_code + .as_ref() + .and_then(|a| a.key(HTTP_RESPONSE_STATUS_CODE)) + { attrs.push(KeyValue::new( - HTTP_RESPONSE_STATUS_CODE, + key, StatusCode::INTERNAL_SERVER_ERROR.as_u16() as i64, )); } @@ -757,123 +808,148 @@ impl Selectors for HttpServerAttributes { fn on_request(&self, request: &router::Request) -> Vec { let mut attrs = Vec::new(); - if let Some(true) = &self.http_route { + if let Some(key) = self.http_route.as_ref().and_then(|a| a.key(HTTP_ROUTE)) { attrs.push(KeyValue::new( - HTTP_ROUTE, + key, request.router_request.uri().path().to_string(), )); } - if let Some(true) = &self.client_address { + if let Some(key) = self + .client_address + .as_ref() + .and_then(|a| a.key(CLIENT_ADDRESS)) + { if let Some(forwarded) = Self::forwarded_for(request) { - attrs.push(KeyValue::new(CLIENT_ADDRESS, forwarded.ip().to_string())); + attrs.push(KeyValue::new(key, forwarded.ip().to_string())); } else if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.peer_address { - attrs.push(KeyValue::new(CLIENT_ADDRESS, socket.ip().to_string())); + attrs.push(KeyValue::new(key, socket.ip().to_string())); } } } - if let Some(true) = &self.client_port { + if let Some(key) = self.client_port.as_ref().and_then(|a| a.key(CLIENT_PORT)) { if let Some(forwarded) = Self::forwarded_for(request) { - attrs.push(KeyValue::new(CLIENT_PORT, forwarded.port() as i64)); + attrs.push(KeyValue::new(key, forwarded.port() as i64)); } else if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.peer_address { - attrs.push(KeyValue::new(CLIENT_PORT, socket.port() as i64)); + attrs.push(KeyValue::new(key, socket.port() as i64)); } } } - if let Some(true) = &self.server_address { + if let Some(key) = self + .server_address + .as_ref() + .and_then(|a| a.key(SERVER_ADDRESS)) + { if let Some(forwarded) = Self::forwarded_host(request).and_then(|h| h.host().map(|h| h.to_string())) { - attrs.push(KeyValue::new(SERVER_ADDRESS, forwarded)); + attrs.push(KeyValue::new(key, forwarded)); } else if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.server_address { - attrs.push(KeyValue::new(SERVER_ADDRESS, socket.ip().to_string())); + attrs.push(KeyValue::new(key, socket.ip().to_string())); } } } - if let Some(true) = &self.server_port { + if let Some(key) = self.server_port.as_ref().and_then(|a| a.key(SERVER_PORT)) { if let Some(forwarded) = Self::forwarded_host(request).and_then(|h| h.port_u16()) { - attrs.push(KeyValue::new(SERVER_PORT, forwarded as i64)); + attrs.push(KeyValue::new(key, forwarded as i64)); } else if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.server_address { - attrs.push(KeyValue::new(SERVER_PORT, socket.port() as i64)); + attrs.push(KeyValue::new(key, socket.port() as i64)); } } } - if let Some(true) = &self.network_local_address { + if let Some(key) = self + .network_local_address + .as_ref() + .and_then(|a| a.key(NETWORK_LOCAL_ADDRESS)) + { if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.server_address { - attrs.push(KeyValue::new( - NETWORK_LOCAL_ADDRESS, - socket.ip().to_string(), - )); + attrs.push(KeyValue::new(key, socket.ip().to_string())); } } } - if let Some(true) = &self.network_local_port { + if let Some(key) = self + .network_local_port + .as_ref() + .and_then(|a| a.key(NETWORK_LOCAL_PORT)) + { if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.server_address { - attrs.push(KeyValue::new(NETWORK_LOCAL_PORT, socket.port() as i64)); + attrs.push(KeyValue::new(key, socket.port() as i64)); } } } - if let Some(true) = &self.network_peer_address { + if let Some(key) = self + .network_peer_address + .as_ref() + .and_then(|a| a.key(NETWORK_PEER_ADDRESS)) + { if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.peer_address { - attrs.push(KeyValue::new(NETWORK_PEER_ADDRESS, socket.ip().to_string())); + attrs.push(KeyValue::new(key, socket.ip().to_string())); } } } - if let Some(true) = &self.network_peer_port { + if let Some(key) = self + .network_peer_port + .as_ref() + .and_then(|a| a.key(NETWORK_PEER_PORT)) + { if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.peer_address { - attrs.push(KeyValue::new(NETWORK_PEER_PORT, socket.port() as i64)); + attrs.push(KeyValue::new(key, socket.port() as i64)); } } } let router_uri = request.router_request.uri(); - if let Some(true) = &self.url_path { - attrs.push(KeyValue::new(URL_PATH, router_uri.path().to_string())); + if let Some(key) = self.url_path.as_ref().and_then(|a| a.key(URL_PATH)) { + attrs.push(KeyValue::new(key, router_uri.path().to_string())); } - if let Some(true) = &self.url_query { + if let Some(key) = self.url_query.as_ref().and_then(|a| a.key(URL_QUERY)) { if let Some(query) = router_uri.query() { - attrs.push(KeyValue::new(URL_QUERY, query.to_string())); + attrs.push(KeyValue::new(key, query.to_string())); } } - if let Some(true) = &self.url_scheme { + if let Some(key) = self.url_scheme.as_ref().and_then(|a| a.key(URL_SCHEME)) { if let Some(scheme) = router_uri.scheme_str() { - attrs.push(KeyValue::new(URL_SCHEME, scheme.to_string())); + attrs.push(KeyValue::new(key, scheme.to_string())); } } - if let Some(true) = &self.user_agent_original { + if let Some(key) = self + .user_agent_original + .as_ref() + .and_then(|a| a.key(USER_AGENT_ORIGINAL)) + { if let Some(user_agent) = request .router_request .headers() .get(&USER_AGENT) .and_then(|h| h.to_str().ok()) { - attrs.push(KeyValue::new(USER_AGENT_ORIGINAL, user_agent.to_string())); + attrs.push(KeyValue::new(key, user_agent.to_string())); } } @@ -934,33 +1010,39 @@ impl Selectors for SupergraphAttributes { fn on_request(&self, request: &supergraph::Request) -> Vec { let mut attrs = Vec::new(); - if let Some(true) = &self.graphql_document { + if let Some(key) = self + .graphql_document + .as_ref() + .and_then(|a| a.key(GRAPHQL_DOCUMENT)) + { if let Some(query) = &request.supergraph_request.body().query { - attrs.push(KeyValue::new(GRAPHQL_DOCUMENT, query.clone())); + attrs.push(KeyValue::new(key, query.clone())); } } - if let Some(true) = &self.graphql_operation_name { + if let Some(key) = self + .graphql_operation_name + .as_ref() + .and_then(|a| a.key(GRAPHQL_OPERATION_NAME)) + { if let Some(operation_name) = &request .context .get::<_, String>(OPERATION_NAME) .unwrap_or_default() { - attrs.push(KeyValue::new( - GRAPHQL_OPERATION_NAME, - operation_name.clone(), - )); + attrs.push(KeyValue::new(key, operation_name.clone())); } } - if let Some(true) = &self.graphql_operation_type { + if let Some(key) = self + .graphql_operation_type + .as_ref() + .and_then(|a| a.key(GRAPHQL_OPERATION_TYPE)) + { if let Some(operation_type) = &request .context .get::<_, String>(OPERATION_KIND) .unwrap_or_default() { - attrs.push(KeyValue::new( - GRAPHQL_OPERATION_TYPE, - operation_type.clone(), - )); + attrs.push(KeyValue::new(key, operation_type.clone())); } } @@ -995,35 +1077,45 @@ impl Selectors for SubgraphAttributes { fn on_request(&self, request: &subgraph::Request) -> Vec { let mut attrs = Vec::new(); - if let Some(true) = &self.graphql_document { + if let Some(key) = self + .graphql_document + .as_ref() + .and_then(|a| a.key(SUBGRAPH_GRAPHQL_DOCUMENT)) + { if let Some(query) = &request.subgraph_request.body().query { - attrs.push(KeyValue::new(SUBGRAPH_GRAPHQL_DOCUMENT, query.clone())); + attrs.push(KeyValue::new(key, query.clone())); } } - if let Some(true) = &self.graphql_operation_name { + if let Some(key) = self + .graphql_operation_name + .as_ref() + .and_then(|a| a.key(SUBGRAPH_GRAPHQL_OPERATION_NAME)) + { if let Some(op_name) = &request.subgraph_request.body().operation_name { - attrs.push(KeyValue::new( - SUBGRAPH_GRAPHQL_OPERATION_NAME, - op_name.clone(), - )); + attrs.push(KeyValue::new(key, op_name.clone())); } } - if let Some(true) = &self.graphql_operation_type { + if let Some(key) = self + .graphql_operation_type + .as_ref() + .and_then(|a| a.key(SUBGRAPH_GRAPHQL_OPERATION_TYPE)) + { // Subgraph operation type wil always match the supergraph operation type if let Some(operation_type) = &request .context .get::<_, String>(OPERATION_KIND) .unwrap_or_default() { - attrs.push(KeyValue::new( - SUBGRAPH_GRAPHQL_OPERATION_TYPE, - operation_type.clone(), - )); + attrs.push(KeyValue::new(key, operation_type.clone())); } } - if let Some(true) = &self.subgraph_name { + if let Some(key) = self + .subgraph_name + .as_ref() + .and_then(|a| a.key(SUBGRAPH_NAME)) + { if let Some(subgraph_name) = &request.subgraph_name { - attrs.push(KeyValue::new(SUBGRAPH_NAME, subgraph_name.clone())); + attrs.push(KeyValue::new(key, subgraph_name.clone())); } } @@ -1090,6 +1182,7 @@ mod test { use crate::plugins::telemetry::config_new::attributes::HttpCommonAttributes; use crate::plugins::telemetry::config_new::attributes::HttpServerAttributes; use crate::plugins::telemetry::config_new::attributes::RouterAttributes; + use crate::plugins::telemetry::config_new::attributes::StandardAttribute; use crate::plugins::telemetry::config_new::attributes::SubgraphAttributes; use crate::plugins::telemetry::config_new::attributes::SupergraphAttributes; use crate::plugins::telemetry::config_new::attributes::ERROR_TYPE; @@ -1129,8 +1222,8 @@ mod test { let _guard = span.enter(); let attributes = RouterAttributes { - datadog_trace_id: Some(true), - trace_id: Some(true), + datadog_trace_id: Some(StandardAttribute::Bool(true)), + trace_id: Some(StandardAttribute::Bool(true)), baggage: Some(true), common: Default::default(), server: Default::default(), @@ -1171,13 +1264,45 @@ mod test { .map(|key_val| &key_val.value), Some(&"baggage_value_bis".into()) ); + + let attributes = RouterAttributes { + datadog_trace_id: Some(StandardAttribute::Aliased { + alias: "datatoutou_id".to_string(), + }), + trace_id: Some(StandardAttribute::Aliased { + alias: "my_trace_id".to_string(), + }), + baggage: Some(false), + common: Default::default(), + server: Default::default(), + }; + let attributes = + attributes.on_request(&router::Request::fake_builder().build().unwrap()); + + assert_eq!( + attributes + .iter() + .find( + |key_val| key_val.key == opentelemetry::Key::from_static_str("my_trace_id") + ) + .map(|key_val| &key_val.value), + Some(&"0000000000000000000000000000002a".into()) + ); + assert_eq!( + attributes + .iter() + .find(|key_val| key_val.key + == opentelemetry::Key::from_static_str("datatoutou_id")) + .map(|key_val| &key_val.value), + Some(&"42".into()) + ); }); } #[test] fn test_supergraph_graphql_document() { let attributes = SupergraphAttributes { - graphql_document: Some(true), + graphql_document: Some(StandardAttribute::Bool(true)), ..Default::default() }; let attributes = attributes.on_request( @@ -1198,7 +1323,7 @@ mod test { #[test] fn test_supergraph_graphql_operation_name() { let attributes = SupergraphAttributes { - graphql_operation_name: Some(true), + graphql_operation_name: Some(StandardAttribute::Bool(true)), ..Default::default() }; let context = crate::Context::new(); @@ -1216,12 +1341,33 @@ mod test { .map(|key_val| &key_val.value), Some(&"topProducts".into()) ); + let attributes = SupergraphAttributes { + graphql_operation_name: Some(StandardAttribute::Aliased { + alias: String::from("graphql_query"), + }), + ..Default::default() + }; + let context = crate::Context::new(); + let _ = context.insert(OPERATION_NAME, "topProducts".to_string()); + let attributes = attributes.on_request( + &supergraph::Request::fake_builder() + .context(context) + .build() + .unwrap(), + ); + assert_eq!( + attributes + .iter() + .find(|key_val| key_val.key.as_str() == "graphql_query") + .map(|key_val| &key_val.value), + Some(&"topProducts".into()) + ); } #[test] fn test_supergraph_graphql_operation_type() { let attributes = SupergraphAttributes { - graphql_operation_type: Some(true), + graphql_operation_type: Some(StandardAttribute::Bool(true)), ..Default::default() }; let context = crate::Context::new(); @@ -1244,7 +1390,7 @@ mod test { #[test] fn test_subgraph_graphql_document() { let attributes = SubgraphAttributes { - graphql_document: Some(true), + graphql_document: Some(StandardAttribute::Bool(true)), ..Default::default() }; let attributes = attributes.on_request( @@ -1273,7 +1419,7 @@ mod test { #[test] fn test_subgraph_graphql_operation_name() { let attributes = SubgraphAttributes { - graphql_operation_name: Some(true), + graphql_operation_name: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1303,7 +1449,7 @@ mod test { #[test] fn test_subgraph_graphql_operation_type() { let attributes = SubgraphAttributes { - graphql_operation_type: Some(true), + graphql_operation_type: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1332,7 +1478,7 @@ mod test { #[test] fn test_subgraph_name() { let attributes = SubgraphAttributes { - subgraph_name: Some(true), + subgraph_name: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1359,7 +1505,7 @@ mod test { #[test] fn test_http_common_error_type() { let common = HttpCommonAttributes { - error_type: Some(true), + error_type: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1400,7 +1546,7 @@ mod test { #[test] fn test_http_common_request_body_size() { let common = HttpCommonAttributes { - http_request_body_size: Some(true), + http_request_body_size: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1425,7 +1571,7 @@ mod test { #[test] fn test_http_common_response_body_size() { let common = HttpCommonAttributes { - http_response_body_size: Some(true), + http_response_body_size: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1450,7 +1596,7 @@ mod test { #[test] fn test_http_common_request_method() { let common = HttpCommonAttributes { - http_request_method: Some(true), + http_request_method: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1472,7 +1618,7 @@ mod test { #[test] fn test_http_common_response_status_code() { let common = HttpCommonAttributes { - http_response_status_code: Some(true), + http_response_status_code: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1503,7 +1649,7 @@ mod test { #[test] fn test_http_common_network_protocol_name() { let common = HttpCommonAttributes { - network_protocol_name: Some(true), + network_protocol_name: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1525,7 +1671,7 @@ mod test { #[test] fn test_http_common_network_protocol_version() { let common = HttpCommonAttributes { - network_protocol_version: Some(true), + network_protocol_version: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1547,7 +1693,7 @@ mod test { #[test] fn test_http_common_network_transport() { let common = HttpCommonAttributes { - network_transport: Some(true), + network_transport: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1564,7 +1710,7 @@ mod test { #[test] fn test_http_common_network_type() { let common = HttpCommonAttributes { - network_type: Some(true), + network_type: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1586,7 +1732,7 @@ mod test { #[test] fn test_http_server_client_address() { let server = HttpServerAttributes { - client_address: Some(true), + client_address: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1625,7 +1771,7 @@ mod test { #[test] fn test_http_server_client_port() { let server = HttpServerAttributes { - client_port: Some(true), + client_port: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1664,7 +1810,7 @@ mod test { #[test] fn test_http_server_http_route() { let server = HttpServerAttributes { - http_route: Some(true), + http_route: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1689,7 +1835,7 @@ mod test { #[test] fn test_http_server_network_local_address() { let server = HttpServerAttributes { - network_local_address: Some(true), + network_local_address: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1714,7 +1860,7 @@ mod test { #[test] fn test_http_server_network_local_port() { let server = HttpServerAttributes { - network_local_port: Some(true), + network_local_port: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1739,7 +1885,7 @@ mod test { #[test] fn test_http_server_network_peer_address() { let server = HttpServerAttributes { - network_peer_address: Some(true), + network_peer_address: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1764,7 +1910,7 @@ mod test { #[test] fn test_http_server_network_peer_port() { let server = HttpServerAttributes { - network_peer_port: Some(true), + network_peer_port: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1789,7 +1935,7 @@ mod test { #[test] fn test_http_server_server_address() { let server = HttpServerAttributes { - server_address: Some(true), + server_address: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1828,7 +1974,7 @@ mod test { #[test] fn test_http_server_server_port() { let server = HttpServerAttributes { - server_port: Some(true), + server_port: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1866,7 +2012,7 @@ mod test { #[test] fn test_http_server_url_path() { let server = HttpServerAttributes { - url_path: Some(true), + url_path: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1887,7 +2033,7 @@ mod test { #[test] fn test_http_server_query() { let server = HttpServerAttributes { - url_query: Some(true), + url_query: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1908,7 +2054,7 @@ mod test { #[test] fn test_http_server_scheme() { let server = HttpServerAttributes { - url_scheme: Some(true), + url_scheme: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1930,7 +2076,7 @@ mod test { #[test] fn test_http_server_user_agent_original() { let server = HttpServerAttributes { - user_agent_original: Some(true), + user_agent_original: Some(StandardAttribute::Bool(true)), ..Default::default() }; diff --git a/apollo-router/src/plugins/telemetry/config_new/cache/attributes.rs b/apollo-router/src/plugins/telemetry/config_new/cache/attributes.rs index 9d072cfa3c..00d0b4b240 100644 --- a/apollo-router/src/plugins/telemetry/config_new/cache/attributes.rs +++ b/apollo-router/src/plugins/telemetry/config_new/cache/attributes.rs @@ -3,6 +3,7 @@ use schemars::JsonSchema; use serde::Deserialize; use tower::BoxError; +use crate::plugins::telemetry::config_new::attributes::StandardAttribute; use crate::plugins::telemetry::config_new::DefaultAttributeRequirementLevel; use crate::plugins::telemetry::config_new::DefaultForLevel; use crate::plugins::telemetry::config_new::Selectors; @@ -14,8 +15,8 @@ use crate::Context; #[serde(deny_unknown_fields, default)] pub(crate) struct CacheAttributes { /// Entity type - #[serde(rename = "entity.type")] - pub(crate) entity_type: Option, + #[serde(rename = "graphql.type.name")] + pub(crate) entity_type: Option, } impl DefaultForLevel for CacheAttributes { @@ -26,7 +27,8 @@ impl DefaultForLevel for CacheAttributes { ) { if let TelemetryDataKind::Metrics = kind { if let DefaultAttributeRequirementLevel::Required = requirement_level { - self.entity_type.get_or_insert(false); + self.entity_type + .get_or_insert(StandardAttribute::Bool(false)); } } } diff --git a/apollo-router/src/plugins/telemetry/config_new/cache/mod.rs b/apollo-router/src/plugins/telemetry/config_new/cache/mod.rs index adc172911b..7000278edc 100644 --- a/apollo-router/src/plugins/telemetry/config_new/cache/mod.rs +++ b/apollo-router/src/plugins/telemetry/config_new/cache/mod.rs @@ -24,7 +24,7 @@ use crate::services::subgraph; pub(crate) mod attributes; pub(crate) const CACHE_METRIC: &str = "apollo.router.operations.entity.cache"; -const ENTITY_TYPE: Key = Key::from_static_str("entity.type"); +const ENTITY_TYPE: Key = Key::from_static_str("graphql.type.name"); const CACHE_HIT: Key = Key::from_static_str("cache.hit"); #[derive(Deserialize, JsonSchema, Clone, Default, Debug)] @@ -93,14 +93,14 @@ impl Instrumented for CacheInstruments { inner_cache_hit.selector = Some(Arc::new(SubgraphSelector::StaticField { r#static: AttributeValue::I64(*hit as i64), })); - if inner_cache_hit + if let Some(key) = inner_cache_hit .selectors .as_ref() - .map(|s| s.attributes.entity_type == Some(true)) - .unwrap_or_default() + .and_then(|s| s.attributes.entity_type.as_ref()) + .and_then(|a| a.key(ENTITY_TYPE)) { inner_cache_hit.attributes.push(KeyValue::new( - ENTITY_TYPE, + key, opentelemetry::Value::String(entity_type.to_string().into()), )); } @@ -118,14 +118,14 @@ impl Instrumented for CacheInstruments { inner_cache_miss.selector = Some(Arc::new(SubgraphSelector::StaticField { r#static: AttributeValue::I64(*miss as i64), })); - if inner_cache_miss + if let Some(key) = inner_cache_miss .selectors .as_ref() - .map(|s| s.attributes.entity_type == Some(true)) - .unwrap_or_default() + .and_then(|s| s.attributes.entity_type.as_ref()) + .and_then(|a| a.key(ENTITY_TYPE)) { inner_cache_miss.attributes.push(KeyValue::new( - ENTITY_TYPE, + key, opentelemetry::Value::String(entity_type.to_string().into()), )); } diff --git a/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs b/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs index 503b191904..31693b6a31 100644 --- a/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs +++ b/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs @@ -9,6 +9,7 @@ use schemars::JsonSchema; use serde::Deserialize; use tower::BoxError; +use super::attributes::StandardAttribute; use super::instruments::Increment; use super::instruments::StaticInstrument; use crate::metrics; @@ -47,16 +48,16 @@ static COST_DELTA: &str = "cost.delta"; pub(crate) struct SupergraphCostAttributes { /// The estimated cost of the operation using the currently configured cost model #[serde(rename = "cost.estimated")] - cost_estimated: Option, + cost_estimated: Option, /// The actual cost of the operation using the currently configured cost model #[serde(rename = "cost.actual")] - cost_actual: Option, + cost_actual: Option, /// The delta (estimated - actual) cost of the operation using the currently configured cost model #[serde(rename = "cost.delta")] - cost_delta: Option, + cost_delta: Option, /// The cost result, this is an error code returned by the cost calculation or COST_OK #[serde(rename = "cost.result")] - cost_result: Option, + cost_result: Option, } impl Selectors for SupergraphCostAttributes { @@ -82,17 +83,33 @@ impl Selectors for SupergraphCostAttributes { .extensions() .with_lock(|lock| lock.get::().cloned()); if let Some(cost_result) = cost_result { - if let Some(true) = self.cost_estimated { - attrs.push(KeyValue::new("cost.estimated", cost_result.estimated)); + if let Some(key) = self + .cost_estimated + .as_ref() + .and_then(|a| a.key(Key::from_static_str("cost.estimated"))) + { + attrs.push(KeyValue::new(key, cost_result.estimated)); } - if let Some(true) = self.cost_actual { - attrs.push(KeyValue::new("cost.actual", cost_result.actual)); + if let Some(key) = self + .cost_actual + .as_ref() + .and_then(|a| a.key(Key::from_static_str("cost.actual"))) + { + attrs.push(KeyValue::new(key, cost_result.actual)); } - if let Some(true) = self.cost_delta { - attrs.push(KeyValue::new("cost.delta", cost_result.delta())); + if let Some(key) = self + .cost_delta + .as_ref() + .and_then(|a| a.key(Key::from_static_str("cost.delta"))) + { + attrs.push(KeyValue::new(key, cost_result.delta())); } - if let Some(true) = self.cost_result { - attrs.push(KeyValue::new("cost.result", cost_result.result)); + if let Some(key) = self + .cost_result + .as_ref() + .and_then(|a| a.key(Key::from_static_str("cost.result"))) + { + attrs.push(KeyValue::new(key, cost_result.result)); } } attrs diff --git a/apollo-router/src/plugins/telemetry/config_new/extendable.rs b/apollo-router/src/plugins/telemetry/config_new/extendable.rs index 6af5d2bf1c..c515a352bd 100644 --- a/apollo-router/src/plugins/telemetry/config_new/extendable.rs +++ b/apollo-router/src/plugins/telemetry/config_new/extendable.rs @@ -283,6 +283,7 @@ mod test { use crate::plugins::telemetry::config_new::attributes::HttpCommonAttributes; use crate::plugins::telemetry::config_new::attributes::HttpServerAttributes; use crate::plugins::telemetry::config_new::attributes::RouterAttributes; + use crate::plugins::telemetry::config_new::attributes::StandardAttribute; use crate::plugins::telemetry::config_new::attributes::SupergraphAttributes; use crate::plugins::telemetry::config_new::conditional::Conditional; use crate::plugins::telemetry::config_new::conditions::Condition; @@ -312,8 +313,8 @@ mod test { extendable_conf.attributes, SupergraphAttributes { graphql_document: None, - graphql_operation_name: Some(true), - graphql_operation_type: Some(true), + graphql_operation_name: Some(StandardAttribute::Bool(true)), + graphql_operation_type: Some(StandardAttribute::Bool(true)), cost: Default::default() } ); @@ -384,12 +385,12 @@ mod test { trace_id: None, baggage: None, common: HttpCommonAttributes { - http_request_method: Some(true), - http_response_status_code: Some(true), + http_request_method: Some(StandardAttribute::Bool(true)), + http_response_status_code: Some(StandardAttribute::Bool(true)), ..Default::default() }, server: HttpServerAttributes { - url_path: Some(true), + url_path: Some(StandardAttribute::Bool(true)), ..Default::default() } } diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/metrics.snap index 5d112315d7..c586792480 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/metrics.snap +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/metrics.snap @@ -10,6 +10,8 @@ info: http.server.active_requests: false http.server.request.duration: attributes: + http.request.method: + alias: http_method my_attribute: request_method: true graphql.operation.name: @@ -23,6 +25,6 @@ info: - sum: 0.1 attributes: graphql.operation.name: TestQuery - http.request.method: GET http.response.status_code: 200 + http_method: GET my_attribute: GET diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/router.yaml index 7efc06671c..ef8332ead1 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/router.yaml +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/router.yaml @@ -5,6 +5,8 @@ telemetry: http.server.active_requests: false http.server.request.duration: attributes: + http.request.method: + alias: http_method my_attribute: request_method: true graphql.operation.name: diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/metrics.snap index 8da724b4c2..49532e0f8f 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/metrics.snap +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/metrics.snap @@ -10,7 +10,8 @@ info: cache: apollo.router.operations.entity.cache: attributes: - entity.type: true + entity.type: + alias: entity_type subgraph.name: subgraph_name: true supergraph.operation.name: @@ -63,25 +64,25 @@ info: - value: 0 attributes: cache.hit: false - entity.type: Product + entity_type: Product subgraph.name: products supergraph.operation.name: Test - value: 0 attributes: cache.hit: false - entity.type: Review + entity_type: Review subgraph.name: products supergraph.operation.name: Test - value: 3 attributes: cache.hit: true - entity.type: Product + entity_type: Product subgraph.name: products supergraph.operation.name: Test - value: 5 attributes: cache.hit: true - entity.type: Review + entity_type: Review subgraph.name: products supergraph.operation.name: Test - name: only_cache_hit_on_subgraph_products diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/router.yaml index b6eee5fb07..f140a9f1e9 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/router.yaml +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/router.yaml @@ -5,7 +5,8 @@ telemetry: cache: apollo.router.operations.entity.cache: attributes: - entity.type: true + graphql.type.name: + alias: entity_type subgraph.name: subgraph_name: true supergraph.operation.name: diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/metrics.snap new file mode 100644 index 0000000000..ae36e59b7d --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/metrics.snap @@ -0,0 +1,23 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: standard instrument http.client.request.duration +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + http.client.request.duration: + attributes: + subgraph.name: + alias: apollo_subgraph_name +--- +- name: http.client.request.duration + description: Duration of HTTP client requests. + unit: s + data: + datapoints: + - sum: 0.1 + attributes: + apollo_subgraph_name: products diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/router.yaml new file mode 100644 index 0000000000..5eb31bceb4 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/router.yaml @@ -0,0 +1,9 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + http.client.request.duration: + attributes: + subgraph.name: + alias: apollo_subgraph_name \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/test.yaml new file mode 100644 index 0000000000..867053ecd4 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/test.yaml @@ -0,0 +1,30 @@ +description: standard instrument http.client.request.duration +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" + - subgraph_response: + status: 200 + data: + hello: "world" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs b/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs index e0267f1482..8765348d78 100644 --- a/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs +++ b/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs @@ -1,11 +1,13 @@ use apollo_compiler::executable::Field; use apollo_compiler::executable::NamedType; +use opentelemetry::Key; use opentelemetry_api::KeyValue; use schemars::JsonSchema; use serde::Deserialize; use serde_json_bytes::Value; use tower::BoxError; +use crate::plugins::telemetry::config_new::attributes::StandardAttribute; use crate::plugins::telemetry::config_new::graphql::selectors::FieldName; use crate::plugins::telemetry::config_new::graphql::selectors::FieldType; use crate::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector; @@ -25,19 +27,19 @@ use crate::Context; pub(crate) struct GraphQLAttributes { /// The GraphQL field name #[serde(rename = "graphql.field.name")] - pub(crate) field_name: Option, + pub(crate) field_name: Option, /// The GraphQL field type #[serde(rename = "graphql.field.type")] - pub(crate) field_type: Option, + pub(crate) field_type: Option, /// If the field is a list, the length of the list #[serde(rename = "graphql.list.length")] - pub(crate) list_length: Option, + pub(crate) list_length: Option, /// The GraphQL operation name #[serde(rename = "graphql.operation.name")] - pub(crate) operation_name: Option, + pub(crate) operation_name: Option, /// The GraphQL type name #[serde(rename = "graphql.type.name")] - pub(crate) type_name: Option, + pub(crate) type_name: Option, } impl DefaultForLevel for GraphQLAttributes { @@ -48,9 +50,9 @@ impl DefaultForLevel for GraphQLAttributes { ) { if let TelemetryDataKind::Metrics = kind { if let DefaultAttributeRequirementLevel::Required = requirement_level { - self.field_name.get_or_insert(true); - self.field_type.get_or_insert(true); - self.type_name.get_or_insert(true); + self.field_name.get_or_insert(StandardAttribute::Bool(true)); + self.field_type.get_or_insert(StandardAttribute::Bool(true)); + self.type_name.get_or_insert(StandardAttribute::Bool(true)); } } } @@ -81,50 +83,70 @@ impl Selectors for GraphQLAttributes { value: &Value, ctx: &Context, ) { - if let Some(true) = self.field_name { + if let Some(key) = self + .field_name + .as_ref() + .and_then(|a| a.key(Key::from_static_str("graphql.field.name"))) + { if let Some(name) = (GraphQLSelector::FieldName { field_name: FieldName::String, }) .on_response_field(ty, field, value, ctx) { - attrs.push(KeyValue::new("graphql.field.name", name)); + attrs.push(KeyValue::new(key, name)); } } - if let Some(true) = self.field_type { + if let Some(key) = self + .field_type + .as_ref() + .and_then(|a| a.key(Key::from_static_str("graphql.field.type"))) + { if let Some(ty) = (GraphQLSelector::FieldType { field_type: FieldType::Name, }) .on_response_field(ty, field, value, ctx) { - attrs.push(KeyValue::new("graphql.field.type", ty)); + attrs.push(KeyValue::new(key, ty)); } } - if let Some(true) = self.type_name { + if let Some(key) = self + .type_name + .as_ref() + .and_then(|a| a.key(Key::from_static_str("graphql.type.name"))) + { if let Some(ty) = (GraphQLSelector::TypeName { type_name: TypeName::String, }) .on_response_field(ty, field, value, ctx) { - attrs.push(KeyValue::new("graphql.type.name", ty)); + attrs.push(KeyValue::new(key, ty)); } } - if let Some(true) = self.list_length { + if let Some(key) = self + .list_length + .as_ref() + .and_then(|a| a.key(Key::from_static_str("graphql.list.length"))) + { if let Some(length) = (GraphQLSelector::ListLength { list_length: ListLength::Value, }) .on_response_field(ty, field, value, ctx) { - attrs.push(KeyValue::new("graphql.list.length", length)); + attrs.push(KeyValue::new(key, length)); } } - if let Some(true) = self.operation_name { + if let Some(key) = self + .operation_name + .as_ref() + .and_then(|a| a.key(Key::from_static_str("graphql.operation.name"))) + { if let Some(length) = (GraphQLSelector::OperationName { operation_name: OperationName::String, default: None, }) .on_response_field(ty, field, value, ctx) { - attrs.push(KeyValue::new("graphql.operation.name", length)); + attrs.push(KeyValue::new(key, length)); } } } @@ -135,6 +157,7 @@ mod test { use serde_json_bytes::json; use crate::context::OPERATION_NAME; + use crate::plugins::telemetry::config_new::attributes::StandardAttribute; use crate::plugins::telemetry::config_new::test::field; use crate::plugins::telemetry::config_new::test::ty; use crate::plugins::telemetry::config_new::DefaultForLevel; @@ -148,9 +171,9 @@ mod test { super::DefaultAttributeRequirementLevel::Required, super::TelemetryDataKind::Metrics, ); - assert_eq!(attributes.field_name, Some(true)); - assert_eq!(attributes.field_type, Some(true)); - assert_eq!(attributes.type_name, Some(true)); + assert_eq!(attributes.field_name, Some(StandardAttribute::Bool(true))); + assert_eq!(attributes.field_type, Some(StandardAttribute::Bool(true))); + assert_eq!(attributes.type_name, Some(StandardAttribute::Bool(true))); assert_eq!(attributes.list_length, None); assert_eq!(attributes.operation_name, None); } @@ -158,11 +181,11 @@ mod test { #[test] fn test_on_response_field_non_list() { let attributes = super::GraphQLAttributes { - field_name: Some(true), - field_type: Some(true), - list_length: Some(true), - operation_name: Some(true), - type_name: Some(true), + field_name: Some(StandardAttribute::Bool(true)), + field_type: Some(StandardAttribute::Bool(true)), + list_length: Some(StandardAttribute::Bool(true)), + operation_name: Some(StandardAttribute::Bool(true)), + type_name: Some(StandardAttribute::Bool(true)), }; let ctx = Context::default(); let _ = ctx.insert(OPERATION_NAME, "operation_name".to_string()); @@ -182,11 +205,11 @@ mod test { #[test] fn test_on_response_field_list() { let attributes = super::GraphQLAttributes { - field_name: Some(true), - field_type: Some(true), - list_length: Some(true), - operation_name: Some(true), - type_name: Some(true), + field_name: Some(StandardAttribute::Bool(true)), + field_type: Some(StandardAttribute::Bool(true)), + list_length: Some(StandardAttribute::Bool(true)), + operation_name: Some(StandardAttribute::Bool(true)), + type_name: Some(StandardAttribute::Bool(true)), }; let ctx = Context::default(); let _ = ctx.insert(OPERATION_NAME, "operation_name".to_string()); diff --git a/apollo-router/src/plugins/telemetry/config_new/spans.rs b/apollo-router/src/plugins/telemetry/config_new/spans.rs index 61dfb0f35c..0abf12b797 100644 --- a/apollo-router/src/plugins/telemetry/config_new/spans.rs +++ b/apollo-router/src/plugins/telemetry/config_new/spans.rs @@ -140,9 +140,11 @@ mod test { use serde_json_bytes::path::JsonPathInst; use crate::context::CONTAINS_GRAPHQL_ERROR; + use crate::context::OPERATION_KIND; use crate::graphql; use crate::plugins::telemetry::config::AttributeValue; use crate::plugins::telemetry::config_new::attributes::DefaultAttributeRequirementLevel; + use crate::plugins::telemetry::config_new::attributes::StandardAttribute; use crate::plugins::telemetry::config_new::attributes::SUBGRAPH_GRAPHQL_DOCUMENT; use crate::plugins::telemetry::config_new::conditional::Conditional; use crate::plugins::telemetry::config_new::conditions::Condition; @@ -553,6 +555,24 @@ mod test { .any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))); } + #[test] + fn test_router_request_standard_attribute_aliased() { + let mut spans = RouterSpans::default(); + spans.attributes.attributes.common.http_request_method = Some(StandardAttribute::Aliased { + alias: String::from("my.method"), + }); + let values = spans.attributes.on_request( + &router::Request::fake_builder() + .method(http::Method::POST) + .header("my-header", "test_val") + .build() + .unwrap(), + ); + assert!(values + .iter() + .any(|key_val| key_val.key == opentelemetry::Key::from_static_str("my.method"))); + } + #[test] fn test_router_response_custom_attribute() { let mut spans = RouterSpans::default(); @@ -620,6 +640,28 @@ mod test { .any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))); } + #[test] + fn test_supergraph_standard_attribute_aliased() { + let mut spans = SupergraphSpans::default(); + spans.attributes.attributes.graphql_operation_type = Some(StandardAttribute::Aliased { + alias: String::from("my_op"), + }); + let context = Context::new(); + context.insert(OPERATION_KIND, "Query".to_string()).unwrap(); + let values = spans.attributes.on_request( + &supergraph::Request::fake_builder() + .method(http::Method::POST) + .header("my-header", "test_val") + .query("Query { me { id } }") + .context(context) + .build() + .unwrap(), + ); + assert!(values + .iter() + .any(|key_val| key_val.key == opentelemetry::Key::from_static_str("my_op"))); + } + #[test] fn test_supergraph_response_event_custom_attribute() { let mut spans = SupergraphSpans::default(); diff --git a/docs/source/configuration/entity-caching.mdx b/docs/source/configuration/entity-caching.mdx index a1f1c2610e..7cff65e71a 100644 --- a/docs/source/configuration/entity-caching.mdx +++ b/docs/source/configuration/entity-caching.mdx @@ -211,7 +211,7 @@ telemetry: cache: # Cache instruments configuration apollo.router.operations.entity.cache: # A counter which counts the number of cache hit and miss for subgraph requests attributes: - entity.type: true # Include the entity type name. default: false + graphql.type.name: true # Include the entity type name. default: false subgraph.name: # Custom attributes to include the subgraph name in the metric subgraph_name: true supergraph.operation.name: # Add custom attribute to display the supergraph operation name diff --git a/docs/source/configuration/telemetry/instrumentation/standard-attributes.mdx b/docs/source/configuration/telemetry/instrumentation/standard-attributes.mdx index 4d71bedc64..5df84f52bf 100644 --- a/docs/source/configuration/telemetry/instrumentation/standard-attributes.mdx +++ b/docs/source/configuration/telemetry/instrumentation/standard-attributes.mdx @@ -22,6 +22,21 @@ telemetry: http.response.status_code: true ``` +### Alias + +If you don't want to use the standard name you can still create an alias and use that alias for the name, for example: + +```yaml title="router.yaml" +telemetry: + instrumentation: + spans: + router: + attributes: + # Standard attributes + http.response.status_code: + alias: status_code # It will be named status_code instead of http.response.status_code +``` + ### Attribute configuration reference From 16afe7b049e7ce140c9ed6da2ca483cc0f4708cd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:59:31 +0200 Subject: [PATCH 10/56] fix(deps): update cargo pre-1.0 packages (minor) (#3628) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Geoffroy Couprie --- Cargo.lock | 187 +++++++++--------- Cargo.toml | 2 +- .../scaffold-test/Cargo.toml | 2 +- apollo-router/Cargo.toml | 26 +-- ...iguration@datadog_enabled.router.yaml.snap | 1 - fuzz/Cargo.toml | 2 +- fuzz/subgraph/Cargo.toml | 2 +- xtask/Cargo.lock | 38 ++-- xtask/Cargo.toml | 10 +- 9 files changed, 134 insertions(+), 136 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb39374ccf..38dbfa5129 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,7 +250,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-types", "axum", - "base64 0.21.7", + "base64 0.22.1", "basic-toml", "bloomfilter", "brotli 3.5.0", @@ -276,7 +276,7 @@ dependencies = [ "futures", "futures-test", "graphql_client", - "heck 0.4.1", + "heck 0.5.0", "hex", "hmac", "http 0.2.12", @@ -289,7 +289,7 @@ dependencies = [ "hyperlocal", "indexmap 2.2.6", "insta", - "itertools 0.12.1", + "itertools 0.13.0", "itoa", "jsonpath-rust", "jsonpath_lib", @@ -308,7 +308,7 @@ dependencies = [ "multer", "multimap 0.9.1", "notify", - "nu-ansi-term 0.49.0", + "nu-ansi-term 0.50.1", "num-traits", "once_cell", "opentelemetry 0.20.0", @@ -360,7 +360,7 @@ dependencies = [ "shellexpand", "similar", "static_assertions", - "strum_macros 0.25.3", + "strum_macros 0.26.4", "sys-info", "tempfile", "test-log", @@ -525,7 +525,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -698,7 +698,7 @@ dependencies = [ "proc-macro2", "quote", "strum 0.25.0", - "syn 2.0.71", + "syn 2.0.76", "thiserror", ] @@ -866,7 +866,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -883,7 +883,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -1457,7 +1457,7 @@ dependencies = [ "proc-macro2", "quote", "str_inflector", - "syn 2.0.71", + "syn 2.0.76", "thiserror", "try_match", ] @@ -1633,7 +1633,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2020,7 +2020,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2031,7 +2031,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2091,7 +2091,7 @@ checksum = "3c65c2ffdafc1564565200967edc4851c7b55422d3913466688907efd05ea26f" dependencies = [ "deno-proc-macro-rules-macros", "proc-macro2", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2103,7 +2103,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2158,7 +2158,7 @@ dependencies = [ "strum 0.25.0", "strum_macros 0.25.3", "syn 1.0.109", - "syn 2.0.71", + "syn 2.0.76", "thiserror", ] @@ -2239,7 +2239,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2252,7 +2252,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.0", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2290,12 +2290,6 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - [[package]] name = "digest" version = "0.10.7" @@ -2358,7 +2352,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2464,20 +2458,30 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", +] + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", ] [[package]] name = "env_logger" -version = "0.10.2" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" dependencies = [ + "anstream", + "anstyle", + "env_filter", "humantime", - "is-terminal", "log", - "regex", - "termcolor", ] [[package]] @@ -2727,7 +2731,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2895,7 +2899,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2994,7 +2998,7 @@ checksum = "b0e085ded9f1267c32176b40921b9754c474f7dd96f7e808d4a982e48aa1e854" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -3070,9 +3074,9 @@ dependencies = [ [[package]] name = "graphql_client" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cdf7b487d864c2939b23902291a5041bc4a84418268f25fda1c8d4e15ad8fa" +checksum = "a50cfdc7f34b7f01909d55c2dcb71d4c13cbcbb4a1605d6c8bd760d654c1144b" dependencies = [ "graphql_query_derive", "serde", @@ -3081,9 +3085,9 @@ dependencies = [ [[package]] name = "graphql_client_codegen" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a40f793251171991c4eb75bd84bc640afa8b68ff6907bc89d3b712a22f700506" +checksum = "5e27ed0c2cf0c0cc52c6bcf3b45c907f433015e580879d14005386251842fb0a" dependencies = [ "graphql-introspection-query", "graphql-parser", @@ -3098,9 +3102,9 @@ dependencies = [ [[package]] name = "graphql_query_derive" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bda454f3d313f909298f626115092d348bc231025699f557b27e248475f48c" +checksum = "83febfa838f898cfa73dfaa7a8eb69ff3409021ac06ee94cfb3d622f6eeb1a97" dependencies = [ "graphql_client_codegen", "proc-macro2", @@ -3996,7 +4000,7 @@ checksum = "f8dccda732e04fa3baf2e17cf835bfe2601c7c2edafd64417c627dabae3a8cda" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -4164,14 +4168,13 @@ dependencies = [ [[package]] name = "mockall" -version = "0.11.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +checksum = "d4c28b3fb6d753d28c20e826cd46ee611fda1cf3cde03a443a974043247c065a" dependencies = [ "cfg-if", "downcast", "fragile", - "lazy_static", "mockall_derive", "predicates", "predicates-tree", @@ -4179,14 +4182,14 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.11.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +checksum = "341014e7f530314e9a1fdbc7400b244efea7122662c96bfa248c31da5bfb2020" dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.76", ] [[package]] @@ -4241,12 +4244,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - [[package]] name = "notify" version = "6.1.1" @@ -4276,11 +4273,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.49.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4853,7 +4850,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -4896,7 +4893,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -4974,7 +4971,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -5028,16 +5025,12 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "predicates" -version = "2.1.5" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" dependencies = [ - "difflib", - "float-cmp", - "itertools 0.10.5", - "normalize-line-endings", + "anstyle", "predicates-core", - "regex", ] [[package]] @@ -5189,7 +5182,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -5587,7 +5580,7 @@ checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -5722,7 +5715,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.71", + "syn 2.0.76", "walkdir", ] @@ -5898,7 +5891,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -6007,7 +6000,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -6018,7 +6011,7 @@ checksum = "afb2522c2a87137bf6c2b3493127fed12877ef1b9476f074d6664edc98acd8a7" dependencies = [ "quote", "regex", - "syn 2.0.71", + "syn 2.0.76", "thiserror", ] @@ -6030,7 +6023,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -6152,7 +6145,7 @@ checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -6379,7 +6372,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -6392,7 +6385,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -6426,9 +6419,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.71" +version = "2.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" dependencies = [ "proc-macro2", "quote", @@ -6527,18 +6520,18 @@ checksum = "5999e24eaa32083191ba4e425deb75cdf25efefabe5aaccb7446dd0d4122a3f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] name = "test-span" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c12e1076fc168ae4b9870a29cabf575d318f5399f025c2248a8deae057ed12" +checksum = "39ada8d8deb7460522e606aa5a66568a7ef9f61dfeee8a3c563c69f4d43b1c96" dependencies = [ "daggy", "derivative", - "indexmap 1.9.3", + "indexmap 2.2.6", "linked-hash-map", "once_cell", "serde", @@ -6553,13 +6546,13 @@ dependencies = [ [[package]] name = "test-span-macro" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f972445f2c781bb6d47ee4a715db3a0e404a79d977f751fd4eb2b0d44c6eb9d" +checksum = "1a30d7cc64c67cb4ac13f4eeb08252f1ae9f1558cd30a336380da6a0a9cf0aef" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.76", ] [[package]] @@ -6594,7 +6587,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -6650,9 +6643,9 @@ dependencies = [ [[package]] name = "tikv-jemalloc-sys" -version = "0.5.4+5.3.0-patched" +version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9402443cb8fd499b6f327e40565234ff34dbda27460c5b47db0db77443dd85d1" +checksum = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d" dependencies = [ "cc", "libc", @@ -6660,9 +6653,9 @@ dependencies = [ [[package]] name = "tikv-jemallocator" -version = "0.5.4" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965fe0c26be5c56c94e38ba547249074803efd52adfb66de62107d95aab3eaca" +checksum = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865" dependencies = [ "libc", "tikv-jemalloc-sys", @@ -6773,7 +6766,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -7069,7 +7062,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -7181,7 +7174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -7263,7 +7256,7 @@ checksum = "b0a91713132798caecb23c977488945566875e7b61b902fb111979871cbff34e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -7606,7 +7599,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", "wasm-bindgen-shared", ] @@ -7640,7 +7633,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8028,7 +8021,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b332785452..af317aa7d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ hex = { version = "0.4.3", features = ["serde"] } http = "0.2.11" insta = { version = "1.38.0", features = ["json", "redactions", "yaml"] } once_cell = "1.19.0" -reqwest = { version = "0.11.24", default-features = false, features = [ +reqwest = { version = "0.11.0", default-features = false, features = [ "rustls-tls", "rustls-native-certs", "gzip", diff --git a/apollo-router-scaffold/scaffold-test/Cargo.toml b/apollo-router-scaffold/scaffold-test/Cargo.toml index 21bee82592..b2b03bbccd 100644 --- a/apollo-router-scaffold/scaffold-test/Cargo.toml +++ b/apollo-router-scaffold/scaffold-test/Cargo.toml @@ -17,7 +17,7 @@ schemars = "0.8.10" serde = "1.0.149" serde_json = "1.0.79" tokio = { version = "1.17.0", features = ["full"] } -tower = { version = "0.4.12", features = ["full"] } +tower = { version = "0.4.0", features = ["full"] } tracing = "0.1.37" # this makes build scripts and proc macros faster to compile diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 11d470ec24..b71ba76149 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -79,7 +79,7 @@ async-compression = { version = "0.4.6", features = [ ] } async-trait.workspace = true axum = { version = "0.6.20", features = ["headers", "json", "original-uri"] } -base64 = "0.21.7" +base64 = "0.22.0" bloomfilter = "1.0.13" buildstructor = "0.5.4" bytes = "1.6.0" @@ -106,17 +106,17 @@ displaydoc = "0.2" flate2 = "1.0.30" fred = { version = "7.1.2", features = ["enable-rustls"] } futures = { version = "0.3.30", features = ["thread-pool"] } -graphql_client = "0.13.0" +graphql_client = "0.14.0" hex.workspace = true http.workspace = true http-body = "0.4.6" -heck = "0.4.1" +heck = "0.5.0" humantime = "2.1.0" humantime-serde = "1.1.1" hyper = { version = "0.14.28", features = ["server", "client", "stream"] } hyper-rustls = { version = "0.24.2", features = ["http1", "http2"] } indexmap = { version = "2.2.6", features = ["serde"] } -itertools = "0.12.1" +itertools = "0.13.0" jsonpath_lib = "0.3.0" jsonpath-rust = "0.3.5" jsonschema = { version = "0.17.1", default-features = false } @@ -127,15 +127,15 @@ linkme = "0.3.27" lru = "0.12.3" maplit = "1.0.2" mediatype = "0.19.18" -mockall = "0.11.4" +mockall = "0.13.0" mime = "0.3.17" multer = "2.1.0" -multimap = "0.9.1" +multimap = "0.9.1" # Warning: part of the public API # To avoid tokio issues notify = { version = "6.1.1", default-features = false, features = [ "macos_kqueue", ] } -nu-ansi-term = "0.49" +nu-ansi-term = "0.50" num-traits = "0.2.19" once_cell = "1.19.0" @@ -214,7 +214,7 @@ serde_json.workspace = true serde_urlencoded = "0.7.1" serde_yaml = "0.8.26" static_assertions = "1.1.0" -strum_macros = "0.25.3" +strum_macros = "0.26.0" sys-info = "0.9.1" thiserror = "1.0.61" tokio.workspace = true @@ -227,7 +227,7 @@ tonic = { version = "0.9.2", features = [ "gzip", ] } tower.workspace = true -tower-http = { version = "0.4.4", features = [ +tower-http = { version = "0.4.0", features = [ "add-extension", "trace", "cors", @@ -291,7 +291,7 @@ hyperlocal = { version = "0.8.0", default-features = false, features = [ ] } [target.'cfg(target_os = "linux")'.dependencies] -tikv-jemallocator = "0.5.4" +tikv-jemallocator = "0.6.0" [dev-dependencies] axum = { version = "0.6.20", features = [ @@ -306,7 +306,7 @@ futures-test = "0.3.30" insta.workspace = true maplit = "1.0.2" memchr = { version = "2.7.4", default-features = false } -mockall = "0.11.4" +mockall = "0.13.0" num-traits = "0.2.19" once_cell.workspace = true opentelemetry-stdout = { version = "0.1.0", features = ["trace"] } @@ -320,7 +320,7 @@ opentelemetry-proto = { version = "0.5.0", features = [ opentelemetry-datadog = { version = "0.8.0", features = ["reqwest-client"] } p256 = "0.13.2" rand_core = "0.6.4" -reqwest = { version = "0.11.27", default-features = false, features = [ +reqwest = { version = "0.11.0", default-features = false, features = [ "json", "multipart", "stream", @@ -336,7 +336,7 @@ tempfile.workspace = true test-log = { version = "0.2.16", default-features = false, features = [ "trace", ] } -test-span = "0.7.0" +test-span = "0.8.0" basic-toml = "0.1.9" tower-test = "0.4.0" diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@datadog_enabled.router.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@datadog_enabled.router.yaml.snap index e7ada975e5..fff5223715 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@datadog_enabled.router.yaml.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@datadog_enabled.router.yaml.snap @@ -9,4 +9,3 @@ telemetry: datadog: endpoint: default enabled: true - diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index dfbb4c9725..f58f7256a9 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -14,7 +14,7 @@ libfuzzer-sys = "0.4" apollo-compiler.workspace = true apollo-parser.workspace = true apollo-smith.workspace = true -env_logger = "0.10.2" +env_logger = "0.11.0" log = "0.4" reqwest = { workspace = true, features = ["json", "blocking"] } serde_json.workspace = true diff --git a/fuzz/subgraph/Cargo.toml b/fuzz/subgraph/Cargo.toml index 0e8ce3b5c8..99c60ef6bf 100644 --- a/fuzz/subgraph/Cargo.toml +++ b/fuzz/subgraph/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" axum = "0.6.20" async-graphql = "6" async-graphql-axum = "6" -env_logger = "0.10" +env_logger = "0.11" futures = "0.3.17" lazy_static = "1.4.0" log = "0.4.16" diff --git a/xtask/Cargo.lock b/xtask/Cargo.lock index 819a7106ac..0a1c01d258 100644 --- a/xtask/Cargo.lock +++ b/xtask/Cargo.lock @@ -128,6 +128,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -567,9 +573,9 @@ dependencies = [ [[package]] name = "graphql_client" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cdf7b487d864c2939b23902291a5041bc4a84418268f25fda1c8d4e15ad8fa" +checksum = "a50cfdc7f34b7f01909d55c2dcb71d4c13cbcbb4a1605d6c8bd760d654c1144b" dependencies = [ "graphql_query_derive", "reqwest", @@ -579,9 +585,9 @@ dependencies = [ [[package]] name = "graphql_client_codegen" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a40f793251171991c4eb75bd84bc640afa8b68ff6907bc89d3b712a22f700506" +checksum = "5e27ed0c2cf0c0cc52c6bcf3b45c907f433015e580879d14005386251842fb0a" dependencies = [ "graphql-introspection-query", "graphql-parser", @@ -596,9 +602,9 @@ dependencies = [ [[package]] name = "graphql_query_derive" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bda454f3d313f909298f626115092d348bc231025699f557b27e248475f48c" +checksum = "83febfa838f898cfa73dfaa7a8eb69ff3409021ac06ee94cfb3d622f6eeb1a97" dependencies = [ "graphql_client_codegen", "proc-macro2", @@ -795,9 +801,9 @@ checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -900,11 +906,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.49.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1150,7 +1156,7 @@ version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -1265,7 +1271,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64", + "base64 0.21.7", ] [[package]] @@ -2114,7 +2120,7 @@ name = "xtask" version = "1.5.0" dependencies = [ "anyhow", - "base64", + "base64 0.22.1", "camino", "cargo_metadata", "chrono", @@ -2145,9 +2151,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zip" diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 7efbefff1b..b57c3a8759 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -20,17 +20,17 @@ chrono = { version = "0.4.34", default-features = false, features = ["clock"] } console = "0.15.8" dialoguer = "0.11.0" flate2 = "1" -graphql_client = { version = "0.13.0", features = ["reqwest-rustls"] } -itertools = "0.12.1" +graphql_client = { version = "0.14.0", features = ["reqwest-rustls"] } +itertools = "0.13.0" libc = "0.2" memorable-wordlist = "0.1.7" -nu-ansi-term = "0.49" +nu-ansi-term = "0.50" once_cell = "1" regex = "1.10.3" reqwest = { version = "0.11", default-features = false, features = [ "blocking", "rustls-tls", - "rustls-tls-native-roots", + "rustls-tls-native-roots" ] } serde = { version = "1.0.197", features = ["derive"] } serde_json = "1" @@ -43,7 +43,7 @@ walkdir = "2.4.0" xshell = "0.2.6" [target.'cfg(target_os = "macos")'.dependencies] -base64 = "0.21" +base64 = "0.22" zip = { version = "0.6", default-features = false } [dev-dependencies] From 1dab36010695d10e0bbb04c77c9ddb617b2d97c8 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Mon, 9 Sep 2024 15:39:31 +0200 Subject: [PATCH 11/56] Skip creating a JS runtime when it would be unused (#5968) --- apollo-router/src/axum_factory/tests.rs | 12 +- apollo-router/src/introspection.rs | 2 +- .../src/plugins/include_subgraph_errors.rs | 1 + .../src/plugins/traffic_shaping/mod.rs | 1 + .../src/query_planner/bridge_query_planner.rs | 195 ++++++++++-------- .../bridge_query_planner_pool.rs | 19 +- .../query_planner/caching_query_planner.rs | 4 +- apollo-router/src/router_factory.rs | 42 ++-- .../src/services/supergraph/service.rs | 4 +- 9 files changed, 143 insertions(+), 137 deletions(-) diff --git a/apollo-router/src/axum_factory/tests.rs b/apollo-router/src/axum_factory/tests.rs index 703193683a..87198b8f4f 100644 --- a/apollo-router/src/axum_factory/tests.rs +++ b/apollo-router/src/axum_factory/tests.rs @@ -2267,10 +2267,14 @@ async fn test_supergraph_timeout() { let schema = include_str!("..//testdata/minimal_supergraph.graphql"); let schema = Arc::new(Schema::parse(schema, &conf).unwrap()); - let planner = - BridgeQueryPlannerPool::new(schema.clone(), conf.clone(), NonZeroUsize::new(1).unwrap()) - .await - .unwrap(); + let planner = BridgeQueryPlannerPool::new( + Vec::new(), + schema.clone(), + conf.clone(), + NonZeroUsize::new(1).unwrap(), + ) + .await + .unwrap(); // we do the entire supergraph rebuilding instead of using `from_supergraph_mock_callback_and_configuration` // because we need the plugins to apply on the supergraph diff --git a/apollo-router/src/introspection.rs b/apollo-router/src/introspection.rs index 597eb4fa06..e7822639ab 100644 --- a/apollo-router/src/introspection.rs +++ b/apollo-router/src/introspection.rs @@ -17,7 +17,7 @@ const DEFAULT_INTROSPECTION_CACHE_CAPACITY: NonZeroUsize = /// A cache containing our well known introspection queries. pub(crate) struct Introspection { cache: CacheStorage, - planner: Arc>, + pub(crate) planner: Arc>, } impl Introspection { diff --git a/apollo-router/src/plugins/include_subgraph_errors.rs b/apollo-router/src/plugins/include_subgraph_errors.rs index 4f558ced82..5f6eb355d1 100644 --- a/apollo-router/src/plugins/include_subgraph_errors.rs +++ b/apollo-router/src/plugins/include_subgraph_errors.rs @@ -195,6 +195,7 @@ mod test { include_str!("../../../apollo-router-benchmarks/benches/fixtures/supergraph.graphql"); let schema = Schema::parse(schema, &Default::default()).unwrap(); let planner = BridgeQueryPlannerPool::new( + Vec::new(), schema.into(), Default::default(), NonZeroUsize::new(1).unwrap(), diff --git a/apollo-router/src/plugins/traffic_shaping/mod.rs b/apollo-router/src/plugins/traffic_shaping/mod.rs index e4e2eabfa1..e77322fffa 100644 --- a/apollo-router/src/plugins/traffic_shaping/mod.rs +++ b/apollo-router/src/plugins/traffic_shaping/mod.rs @@ -571,6 +571,7 @@ mod test { let config = Arc::new(config); let schema = Arc::new(Schema::parse(schema, &config).unwrap()); let planner = BridgeQueryPlannerPool::new( + Vec::new(), schema.clone(), config.clone(), NonZeroUsize::new(1).unwrap(), diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 30ebd5b36b..baca78982e 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -30,7 +30,7 @@ use tower::Service; use super::PlanNode; use super::QueryKey; use crate::apollo_studio_interop::generate_usage_reporting; -use crate::configuration::IntrospectionMode; +use crate::configuration::IntrospectionMode as IntrospectionConfig; use crate::configuration::QueryPlannerMode; use crate::error::PlanErrors; use crate::error::QueryPlannerError; @@ -79,7 +79,7 @@ pub(crate) struct BridgeQueryPlanner { planner: PlannerMode, schema: Arc, subgraph_schemas: Arc>>>, - introspection: Option>, + introspection: IntrospectionMode, configuration: Arc, enable_authorization_directives: bool, _federation_instrument: ObservableGauge, @@ -93,11 +93,15 @@ pub(crate) enum PlannerMode { js: Arc>, rust: Arc, }, - Rust { - rust: Arc, - // TODO: remove when those other uses are fully ported to Rust - js_for_api_schema_and_introspection_and_operation_signature: Arc>, - }, + Rust(Arc), +} + +#[derive(Clone)] +enum IntrospectionMode { + Js(Arc), + Both(Arc), + Rust, + Disabled, } fn federation_version_instrument(federation_version: Option) -> ObservableGauge { @@ -120,25 +124,19 @@ impl PlannerMode { async fn new( schema: &Schema, configuration: &Configuration, - old_planner: Option>>, + old_planner: &Option>>, rust_planner: Option>, ) -> Result { Ok(match configuration.experimental_query_planner_mode { - QueryPlannerMode::New => Self::Rust { - js_for_api_schema_and_introspection_and_operation_signature: Self::js( - &schema.raw_sdl, - configuration, - old_planner, - ) - .await?, - rust: rust_planner + QueryPlannerMode::New => Self::Rust( + rust_planner .expect("expected Rust QP instance for `experimental_query_planner_mode: new`"), - }, + ), QueryPlannerMode::Legacy => { - Self::Js(Self::js(&schema.raw_sdl, configuration, old_planner).await?) + Self::Js(Self::js_planner(&schema.raw_sdl, configuration, old_planner).await?) } QueryPlannerMode::Both => Self::Both { - js: Self::js(&schema.raw_sdl, configuration, old_planner).await?, + js: Self::js_planner(&schema.raw_sdl, configuration, old_planner).await?, rust: rust_planner.expect( "expected Rust QP instance for `experimental_query_planner_mode: both`", ), @@ -146,11 +144,11 @@ impl PlannerMode { QueryPlannerMode::BothBestEffort => { if let Some(rust) = rust_planner { Self::Both { - js: Self::js(&schema.raw_sdl, configuration, old_planner).await?, + js: Self::js_planner(&schema.raw_sdl, configuration, old_planner).await?, rust, } } else { - Self::Js(Self::js(&schema.raw_sdl, configuration, old_planner).await?) + Self::Js(Self::js_planner(&schema.raw_sdl, configuration, old_planner).await?) } } }) @@ -222,13 +220,13 @@ impl PlannerMode { Ok(Arc::new(result.map_err(ServiceBuildError::QpInitError)?)) } - async fn js( + async fn js_planner( sdl: &str, configuration: &Configuration, - old_planner: Option>>, + old_js_planner: &Option>>, ) -> Result>, ServiceBuildError> { let query_planner_configuration = configuration.js_query_planner_config(); - let planner = match old_planner { + let planner = match old_js_planner { None => Planner::new(sdl.to_owned(), query_planner_configuration).await?, Some(old_planner) => { old_planner @@ -239,17 +237,22 @@ impl PlannerMode { Ok(Arc::new(planner)) } - fn js_for_api_schema_and_introspection_and_operation_signature( + async fn js_introspection( &self, - ) -> &Arc> { - match self { - PlannerMode::Js(js) => js, - PlannerMode::Both { js, .. } => js, - PlannerMode::Rust { - js_for_api_schema_and_introspection_and_operation_signature, - .. - } => js_for_api_schema_and_introspection_and_operation_signature, - } + sdl: &str, + configuration: &Configuration, + old_js_planner: &Option>>, + ) -> Result, ServiceBuildError> { + let js_planner = match self { + Self::Js(js) => js.clone(), + Self::Both { js, .. } => js.clone(), + Self::Rust(_) => { + // JS "planner" (actually runtime) was not created for planning + // but is still needed for introspection, so create it now + Self::js_planner(sdl, configuration, old_js_planner).await? + } + }; + Ok(Arc::new(Introspection::new(js_planner).await?)) } async fn plan( @@ -283,7 +286,7 @@ impl PlannerMode { } Ok(success) } - PlannerMode::Rust { rust, .. } => { + PlannerMode::Rust(rust) => { let start = Instant::now(); let result = operation @@ -368,7 +371,7 @@ impl PlannerMode { let js = match self { PlannerMode::Js(js) => js, PlannerMode::Both { js, .. } => js, - PlannerMode::Rust { rust, .. } => { + PlannerMode::Rust(rust) => { return Ok(rust .subgraph_schemas() .iter() @@ -396,21 +399,26 @@ impl BridgeQueryPlanner { rust_planner: Option>, ) -> Result { let planner = - PlannerMode::new(&schema, &configuration, old_js_planner, rust_planner).await?; + PlannerMode::new(&schema, &configuration, &old_js_planner, rust_planner).await?; let subgraph_schemas = Arc::new(planner.subgraphs().await?); let introspection = if configuration.supergraph.introspection { - Some(Arc::new( - Introspection::new( + match configuration.experimental_introspection_mode { + IntrospectionConfig::New => IntrospectionMode::Rust, + IntrospectionConfig::Legacy => IntrospectionMode::Js( planner - .js_for_api_schema_and_introspection_and_operation_signature() - .clone(), - ) - .await?, - )) + .js_introspection(&schema.raw_sdl, &configuration, &old_js_planner) + .await?, + ), + IntrospectionConfig::Both => IntrospectionMode::Both( + planner + .js_introspection(&schema.raw_sdl, &configuration, &old_js_planner) + .await?, + ), + } } else { - None + IntrospectionMode::Disabled }; let enable_authorization_directives = @@ -431,10 +439,18 @@ impl BridgeQueryPlanner { }) } - pub(crate) fn planner(&self) -> Arc> { - self.planner - .js_for_api_schema_and_introspection_and_operation_signature() - .clone() + pub(crate) fn js_planner(&self) -> Option>> { + match &self.planner { + PlannerMode::Js(js) => Some(js.clone()), + PlannerMode::Both { js, .. } => Some(js.clone()), + PlannerMode::Rust(_) => match &self.introspection { + IntrospectionMode::Js(js_introspection) + | IntrospectionMode::Both(js_introspection) => { + Some(js_introspection.planner.clone()) + } + IntrospectionMode::Rust | IntrospectionMode::Disabled => None, + }, + } } #[cfg(test)] @@ -494,11 +510,17 @@ impl BridgeQueryPlanner { key: QueryKey, doc: ParsedDocument, ) -> Result { - let Some(introspection) = &self.introspection else { - return Ok(QueryPlannerContent::IntrospectionDisabled); - }; - let mode = self.configuration.experimental_introspection_mode; - let response = if mode != IntrospectionMode::New && doc.executable.operations.len() > 1 { + match &self.introspection { + IntrospectionMode::Disabled => return Ok(QueryPlannerContent::IntrospectionDisabled), + IntrospectionMode::Rust => { + return Ok(QueryPlannerContent::Response { + response: Box::new(self.rust_introspection(&key, &doc)?), + }); + } + IntrospectionMode::Js(_) | IntrospectionMode::Both(_) => {} + } + + if doc.executable.operations.len() > 1 { // TODO: add an operation_name parameter to router-bridge to fix this? let error = graphql::Error::builder() .message( @@ -510,43 +532,42 @@ impl BridgeQueryPlanner { return Ok(QueryPlannerContent::Response { response: Box::new(graphql::Response::builder().error(error).build()), }); - } else { - match mode { - IntrospectionMode::Legacy => introspection + } + + let response = match &self.introspection { + IntrospectionMode::Rust | IntrospectionMode::Disabled => unreachable!(), // returned above + IntrospectionMode::Js(js) => js + .execute(key.filtered_query) + .await + .map_err(QueryPlannerError::Introspection)?, + IntrospectionMode::Both(js) => { + let rust_result = match self.rust_introspection(&key, &doc) { + Ok(response) => { + if response.errors.is_empty() { + Ok(response) + } else { + Err(QueryPlannerError::Introspection(IntrospectionError { + message: Some( + response + .errors + .into_iter() + .map(|e| e.to_string()) + .collect::>() + .join(", "), + ), + })) + } + } + Err(e) => Err(e), + }; + let js_result = js .execute(key.filtered_query) .await - .map_err(QueryPlannerError::Introspection)?, - IntrospectionMode::New => self.rust_introspection(&key, &doc)?, - IntrospectionMode::Both => { - let rust_result = match self.rust_introspection(&key, &doc) { - Ok(response) => { - if response.errors.is_empty() { - Ok(response) - } else { - Err(QueryPlannerError::Introspection(IntrospectionError { - message: Some( - response - .errors - .into_iter() - .map(|e| e.to_string()) - .collect::>() - .join(", "), - ), - })) - } - } - Err(e) => Err(e), - }; - let js_result = introspection - .execute(key.filtered_query) - .await - .map_err(QueryPlannerError::Introspection); - self.compare_introspection_responses(js_result.clone(), rust_result); - js_result? - } + .map_err(QueryPlannerError::Introspection); + self.compare_introspection_responses(js_result.clone(), rust_result); + js_result? } }; - Ok(QueryPlannerContent::Response { response: Box::new(response), }) diff --git a/apollo-router/src/query_planner/bridge_query_planner_pool.rs b/apollo-router/src/query_planner/bridge_query_planner_pool.rs index bb75124df1..b27ac14087 100644 --- a/apollo-router/src/query_planner/bridge_query_planner_pool.rs +++ b/apollo-router/src/query_planner/bridge_query_planner_pool.rs @@ -50,14 +50,6 @@ pub(crate) struct BridgeQueryPlannerPool { impl BridgeQueryPlannerPool { pub(crate) async fn new( - schema: Arc, - configuration: Arc, - size: NonZeroUsize, - ) -> Result { - Self::new_from_planners(Default::default(), schema, configuration, size).await - } - - pub(crate) async fn new_from_planners( old_js_planners: Vec>>, schema: Arc, configuration: Arc, @@ -102,9 +94,9 @@ impl BridgeQueryPlannerPool { })? .subgraph_schemas(); - let planners: Vec<_> = bridge_query_planners + let js_planners: Vec<_> = bridge_query_planners .iter() - .map(|p| p.planner().clone()) + .filter_map(|p| p.js_planner()) .collect(); for mut planner in bridge_query_planners.into_iter() { @@ -140,7 +132,7 @@ impl BridgeQueryPlannerPool { let (v8_heap_total, _v8_heap_total_gauge) = Self::create_heap_total_gauge(&meter); // initialize v8 metrics - if let Some(bridge_query_planner) = planners.first().cloned() { + if let Some(bridge_query_planner) = js_planners.first().cloned() { Self::get_v8_metrics( bridge_query_planner, v8_heap_used.clone(), @@ -150,7 +142,7 @@ impl BridgeQueryPlannerPool { } Ok(Self { - js_planners: planners, + js_planners, sender, schema, subgraph_schemas, @@ -190,7 +182,7 @@ impl BridgeQueryPlannerPool { (current_heap_total, heap_total_gauge) } - pub(crate) fn planners(&self) -> Vec>> { + pub(crate) fn js_planners(&self) -> Vec>> { self.js_planners.clone() } @@ -297,6 +289,7 @@ mod tests { async move { let mut pool = BridgeQueryPlannerPool::new( + Vec::new(), schema.clone(), config.clone(), NonZeroUsize::new(2).unwrap(), diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index f6270381cb..e2a4053d96 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -382,8 +382,8 @@ where } impl CachingQueryPlanner { - pub(crate) fn planners(&self) -> Vec>> { - self.delegate.planners() + pub(crate) fn js_planners(&self) -> Vec>> { + self.delegate.js_planners() } pub(crate) fn subgraph_schemas( diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs index 4a5ce3f888..640cbc9aa7 100644 --- a/apollo-router/src/router_factory.rs +++ b/apollo-router/src/router_factory.rs @@ -294,34 +294,20 @@ impl YamlRouterFactory { ) -> Result { let query_planner_span = tracing::info_span!("query_planner_creation"); // QueryPlannerService takes an UnplannedRequest and outputs PlannedRequest - let bridge_query_planner = - match previous_supergraph.as_ref().map(|router| router.planners()) { - None => { - BridgeQueryPlannerPool::new( - schema.clone(), - configuration.clone(), - configuration - .supergraph - .query_planning - .experimental_query_planner_parallelism()?, - ) - .instrument(query_planner_span) - .await? - } - Some(planners) => { - BridgeQueryPlannerPool::new_from_planners( - planners, - schema.clone(), - configuration.clone(), - configuration - .supergraph - .query_planning - .experimental_query_planner_parallelism()?, - ) - .instrument(query_planner_span) - .await? - } - }; + let bridge_query_planner = BridgeQueryPlannerPool::new( + previous_supergraph + .as_ref() + .map(|router| router.js_planners()) + .unwrap_or_default(), + schema.clone(), + configuration.clone(), + configuration + .supergraph + .query_planning + .experimental_query_planner_parallelism()?, + ) + .instrument(query_planner_span) + .await?; let schema_changed = previous_supergraph .map(|supergraph_creator| supergraph_creator.schema().raw_sdl == schema.raw_sdl) diff --git a/apollo-router/src/services/supergraph/service.rs b/apollo-router/src/services/supergraph/service.rs index dec84074f8..d06edb7092 100644 --- a/apollo-router/src/services/supergraph/service.rs +++ b/apollo-router/src/services/supergraph/service.rs @@ -934,8 +934,8 @@ impl SupergraphCreator { self.query_planner_service.previous_cache() } - pub(crate) fn planners(&self) -> Vec>> { - self.query_planner_service.planners() + pub(crate) fn js_planners(&self) -> Vec>> { + self.query_planner_service.js_planners() } pub(crate) async fn warm_up_query_planner( From 29bf3a4b249b003fbc49b4c2af044d88846767de Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Mon, 9 Sep 2024 18:06:17 +0200 Subject: [PATCH 12/56] remove the invalidation task (#5941) --- ...nfiguration__tests__schema_generation.snap | 7 + apollo-router/src/plugins/cache/entity.rs | 12 +- .../src/plugins/cache/invalidation.rs | 269 ++++++++---------- .../plugins/cache/invalidation_endpoint.rs | 220 ++------------ apollo-router/src/plugins/cache/tests.rs | 2 +- 5 files changed, 165 insertions(+), 345 deletions(-) diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index c514cb925d..8789840074 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -3591,6 +3591,13 @@ expression: "&schema" "InvalidationEndpointConfig": { "additionalProperties": false, "properties": { + "concurrent_requests": { + "default": 10, + "description": "Number of concurrent invalidation requests", + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, "listen": { "$ref": "#/definitions/ListenAddr", "description": "#/definitions/ListenAddr" diff --git a/apollo-router/src/plugins/cache/entity.rs b/apollo-router/src/plugins/cache/entity.rs index 9c3dc0f8de..b0abd0178e 100644 --- a/apollo-router/src/plugins/cache/entity.rs +++ b/apollo-router/src/plugins/cache/entity.rs @@ -78,8 +78,8 @@ pub(crate) struct EntityCache { } pub(crate) struct Storage { - all: Option, - subgraphs: HashMap, + pub(crate) all: Option, + pub(crate) subgraphs: HashMap, } impl Storage { @@ -285,6 +285,11 @@ impl Plugin for EntityCache { .as_ref() .map(|i| i.scan_count) .unwrap_or(1000), + init.config + .invalidation + .as_ref() + .map(|i| i.concurrent_requests) + .unwrap_or(10), ) .await?; @@ -456,7 +461,7 @@ impl EntityCache { all: Some(storage), subgraphs: HashMap::new(), }); - let invalidation = Invalidation::new(storage.clone(), 1000).await?; + let invalidation = Invalidation::new(storage.clone(), 1000, 10).await?; Ok(Self { storage, @@ -475,6 +480,7 @@ impl EntityCache { 4000, )), scan_count: 1000, + concurrent_requests: 10, })), invalidation, }) diff --git a/apollo-router/src/plugins/cache/invalidation.rs b/apollo-router/src/plugins/cache/invalidation.rs index 2533f26bfd..45d623299c 100644 --- a/apollo-router/src/plugins/cache/invalidation.rs +++ b/apollo-router/src/plugins/cache/invalidation.rs @@ -3,13 +3,14 @@ use std::time::Instant; use fred::error::RedisError; use fred::types::Scanner; +use futures::stream; use futures::StreamExt; use itertools::Itertools; use serde::Deserialize; use serde::Serialize; use serde_json_bytes::Value; use thiserror::Error; -use tokio::sync::broadcast; +use tokio::sync::Semaphore; use tower::BoxError; use tracing::Instrument; @@ -19,16 +20,11 @@ use crate::cache::redis::RedisKey; use crate::plugins::cache::entity::hash_entity_key; use crate::plugins::cache::entity::ENTITY_CACHE_VERSION; -const CHANNEL_SIZE: usize = 1024; - #[derive(Clone)] pub(crate) struct Invalidation { - #[allow(clippy::type_complexity)] - pub(super) handle: tokio::sync::mpsc::Sender<( - Vec, - InvalidationOrigin, - broadcast::Sender>, - )>, + pub(crate) storage: Arc, + pub(crate) scan_count: u32, + pub(crate) semaphore: Arc, } #[derive(Error, Debug, Clone)] @@ -37,9 +33,6 @@ pub(crate) enum InvalidationError { RedisError(#[from] RedisError), #[error("several errors")] Errors(#[from] InvalidationErrors), - #[cfg(test)] - #[error("custom error: {0}")] - Custom(String), } #[derive(Debug, Clone)] @@ -70,53 +63,20 @@ impl Invalidation { pub(crate) async fn new( storage: Arc, scan_count: u32, + concurrent_requests: u32, ) -> Result { - let (tx, rx) = tokio::sync::mpsc::channel(CHANNEL_SIZE); - - tokio::task::spawn(async move { - start(storage, scan_count, rx).await; - }); - Ok(Self { handle: tx }) + Ok(Self { + storage, + scan_count, + semaphore: Arc::new(Semaphore::new(concurrent_requests as usize)), + }) } pub(crate) async fn invalidate( - &mut self, + &self, origin: InvalidationOrigin, requests: Vec, ) -> Result { - let (response_tx, mut response_rx) = broadcast::channel(2); - self.handle - .send((requests, origin, response_tx.clone())) - .await - .map_err(|e| format!("cannot send invalidation request: {e}"))?; - - let result = response_rx - .recv() - .await - .map_err(|err| { - format!( - "cannot receive response for invalidation request: {:?}", - err - ) - })? - .map_err(|err| format!("received an invalidation error: {:?}", err))?; - - Ok(result) - } -} - -// TODO refactor -#[allow(clippy::type_complexity)] -async fn start( - storage: Arc, - scan_count: u32, - mut handle: tokio::sync::mpsc::Receiver<( - Vec, - InvalidationOrigin, - broadcast::Sender>, - )>, -) { - while let Some((requests, origin, response_tx)) = handle.recv().await { let origin = match origin { InvalidationOrigin::Endpoint => "endpoint", InvalidationOrigin::Extensions => "extensions", @@ -128,117 +88,130 @@ async fn start( "origin" = origin ); - if let Err(err) = response_tx.send( - handle_request_batch(&storage, scan_count, origin, requests) - .instrument(tracing::info_span!( - "cache.invalidation.batch", - "origin" = origin - )) - .await, - ) { - ::tracing::error!("cannot send answer to invalidation request in the channel: {err}"); - } + Ok(self + .handle_request_batch(origin, requests) + .instrument(tracing::info_span!( + "cache.invalidation.batch", + "origin" = origin + )) + .await?) } -} -async fn handle_request( - storage: &RedisCacheStorage, - scan_count: u32, - origin: &'static str, - request: &InvalidationRequest, -) -> Result { - let key_prefix = request.key_prefix(); - let subgraph = request.subgraph_name(); - tracing::debug!( - "got invalidation request: {request:?}, will scan for: {}", - key_prefix - ); - - let mut stream = storage.scan(key_prefix.clone(), Some(scan_count)); - let mut count = 0u64; - let mut error = None; + async fn handle_request( + &self, + redis_storage: &RedisCacheStorage, + origin: &'static str, + request: &InvalidationRequest, + ) -> Result { + let key_prefix = request.key_prefix(); + let subgraph = request.subgraph_name(); + tracing::debug!( + "got invalidation request: {request:?}, will scan for: {}", + key_prefix + ); - while let Some(res) = stream.next().await { - match res { - Err(e) => { - tracing::error!( - pattern = key_prefix, - error = %e, - message = "error scanning for key", - ); - error = Some(e); - break; - } - Ok(scan_res) => { - if let Some(keys) = scan_res.results() { - let keys = keys - .iter() - .filter_map(|k| k.as_str()) - .map(|k| RedisKey(k.to_string())) - .collect::>(); - if !keys.is_empty() { - count += storage.delete(keys).await.unwrap_or(0) as u64; + let mut stream = redis_storage.scan(key_prefix.clone(), Some(self.scan_count)); + let mut count = 0u64; + let mut error = None; - u64_counter!( - "apollo.router.operations.entity.invalidation.entry", - "Entity cache counter for invalidated entries", - count, - "origin" = origin, - "subgraph.name" = subgraph.clone() - ); + while let Some(res) = stream.next().await { + match res { + Err(e) => { + tracing::error!( + pattern = key_prefix, + error = %e, + message = "error scanning for key", + ); + error = Some(e); + break; + } + Ok(scan_res) => { + if let Some(keys) = scan_res.results() { + let keys = keys + .iter() + .filter_map(|k| k.as_str()) + .map(|k| RedisKey(k.to_string())) + .collect::>(); + if !keys.is_empty() { + let deleted = redis_storage.delete(keys).await.unwrap_or(0) as u64; + count += deleted; + } } + scan_res.next()?; } - scan_res.next()?; } } - } - u64_histogram!( - "apollo.router.cache.invalidation.keys", - "Number of invalidated keys.", - count - ); + u64_counter!( + "apollo.router.operations.entity.invalidation.entry", + "Entity cache counter for invalidated entries", + count, + "origin" = origin, + "subgraph.name" = subgraph.clone() + ); - match error { - Some(err) => Err(err.into()), - None => Ok(count), + u64_histogram!( + "apollo.router.cache.invalidation.keys", + "Number of invalidated keys per invalidation request.", + count + ); + + match error { + Some(err) => Err(err.into()), + None => Ok(count), + } } -} -async fn handle_request_batch( - storage: &EntityStorage, - scan_count: u32, - origin: &'static str, - requests: Vec, -) -> Result { - let mut count = 0; - let mut errors = Vec::new(); - for request in requests { - let start = Instant::now(); - let redis_storage = match storage.get(request.subgraph_name()) { - Some(s) => s, - None => continue, - }; - match handle_request(redis_storage, scan_count, origin, &request) - .instrument(tracing::info_span!("cache.invalidation.request")) - .await - { - Ok(c) => count += c, - Err(err) => { - errors.push(err); + async fn handle_request_batch( + &self, + origin: &'static str, + requests: Vec, + ) -> Result { + let mut count = 0; + let mut errors = Vec::new(); + let mut futures = Vec::new(); + for request in requests { + let redis_storage = match self.storage.get(request.subgraph_name()) { + Some(s) => s, + None => continue, + }; + + let semaphore = self.semaphore.clone(); + let f = async move { + // limit the number of invalidation requests executing at any point in time + let _ = semaphore.acquire().await; + + let start = Instant::now(); + + let res = self + .handle_request(redis_storage, origin, &request) + .instrument(tracing::info_span!("cache.invalidation.request")) + .await; + + f64_histogram!( + "apollo.router.cache.invalidation.duration", + "Duration of the invalidation event execution.", + start.elapsed().as_secs_f64() + ); + res + }; + futures.push(f); + } + let mut stream: stream::FuturesUnordered<_> = futures.into_iter().collect(); + while let Some(res) = stream.next().await { + match res { + Ok(c) => count += c, + Err(err) => { + errors.push(err); + } } } - f64_histogram!( - "apollo.router.cache.invalidation.duration", - "Duration of the invalidation event execution.", - start.elapsed().as_secs_f64() - ); - } - if !errors.is_empty() { - Err(InvalidationErrors(errors).into()) - } else { - Ok(count) + if !errors.is_empty() { + Err(InvalidationErrors(errors).into()) + } else { + Ok(count) + } } } diff --git a/apollo-router/src/plugins/cache/invalidation_endpoint.rs b/apollo-router/src/plugins/cache/invalidation_endpoint.rs index 0ef5b7ed8d..0b1af7a9b3 100644 --- a/apollo-router/src/plugins/cache/invalidation_endpoint.rs +++ b/apollo-router/src/plugins/cache/invalidation_endpoint.rs @@ -42,12 +42,19 @@ pub(crate) struct InvalidationEndpointConfig { #[serde(default = "default_scan_count")] /// Number of keys to return at once from a redis SCAN command pub(crate) scan_count: u32, + #[serde(default = "concurrent_requests_count")] + /// Number of concurrent invalidation requests + pub(crate) concurrent_requests: u32, } fn default_scan_count() -> u32 { 1000 } +fn concurrent_requests_count() -> u32 { + 10 +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub(crate) enum InvalidationType { @@ -89,7 +96,7 @@ impl Service for InvalidationService { } fn call(&mut self, req: router::Request) -> Self::Future { - let mut invalidation = self.invalidation.clone(); + let invalidation = self.invalidation.clone(); let config = self.config.clone(); Box::pin( async move { @@ -210,16 +217,23 @@ fn valid_shared_key( mod tests { use std::collections::HashMap; - use tokio::sync::broadcast; use tower::ServiceExt; use super::*; - use crate::plugins::cache::invalidation::InvalidationError; + use crate::cache::redis::RedisCacheStorage; + use crate::plugins::cache::entity::Storage; + use crate::plugins::cache::tests::MockStore; #[tokio::test] async fn test_invalidation_service_bad_shared_key() { - let (handle, _rx) = tokio::sync::mpsc::channel(128); - let invalidation = Invalidation { handle }; + let redis_cache = RedisCacheStorage::from_mocks(Arc::new(MockStore::new())) + .await + .unwrap(); + let storage = Arc::new(Storage { + all: Some(redis_cache), + subgraphs: HashMap::new(), + }); + let invalidation = Invalidation::new(storage.clone(), 1000, 10).await.unwrap(); let config = Arc::new(SubgraphConfiguration { all: Subgraph { @@ -257,108 +271,16 @@ mod tests { } #[tokio::test] - async fn test_invalidation_service_good_sub_shared_key() { - let (handle, mut rx) = tokio::sync::mpsc::channel::<( - Vec, - InvalidationOrigin, - broadcast::Sender>, - )>(128); - tokio::task::spawn(async move { - let mut called = false; - while let Some((requests, origin, response_tx)) = rx.recv().await { - called = true; - if requests - != [ - InvalidationRequest::Subgraph { - subgraph: String::from("test"), - }, - InvalidationRequest::Type { - subgraph: String::from("test"), - r#type: String::from("Test"), - }, - ] - { - response_tx - .send(Err(InvalidationError::Custom(format!( - "it's not the right invalidation requests : {requests:?}" - )))) - .unwrap(); - return; - } - if origin != InvalidationOrigin::Endpoint { - response_tx - .send(Err(InvalidationError::Custom(format!( - "it's not the right invalidation origin : {origin:?}" - )))) - .unwrap(); - return; - } - response_tx.send(Ok(0)).unwrap(); - } - assert!(called); - }); - - let invalidation = Invalidation { - handle: handle.clone(), - }; - let config = Arc::new(SubgraphConfiguration { - all: Subgraph { - ttl: None, - enabled: true, - redis: None, - private_id: None, - invalidation: Some(SubgraphInvalidationConfig { - enabled: true, - shared_key: String::from("test"), - }), - }, - subgraphs: [( - String::from("test"), - Subgraph { - ttl: None, - redis: None, - enabled: true, - private_id: None, - invalidation: Some(SubgraphInvalidationConfig { - enabled: true, - shared_key: String::from("test_test"), - }), - }, - )] - .into_iter() - .collect(), - }); - let service = InvalidationService::new(config, invalidation); - let req = router::Request::fake_builder() - .method(http::Method::POST) - .header(AUTHORIZATION, "test_test") - .body( - serde_json::to_vec(&[ - InvalidationRequest::Subgraph { - subgraph: String::from("test"), - }, - InvalidationRequest::Type { - subgraph: String::from("test"), - r#type: String::from("Test"), - }, - ]) - .unwrap(), - ) - .build() + async fn test_invalidation_service_bad_shared_key_subgraph() { + let redis_cache = RedisCacheStorage::from_mocks(Arc::new(MockStore::new())) + .await .unwrap(); - let res = service.oneshot(req).await.unwrap(); - assert_eq!(res.response.status(), StatusCode::ACCEPTED); - } + let storage = Arc::new(Storage { + all: Some(redis_cache), + subgraphs: HashMap::new(), + }); + let invalidation = Invalidation::new(storage.clone(), 1000, 10).await.unwrap(); - #[tokio::test] - async fn test_invalidation_service_bad_shared_key_subgraph() { - #[allow(clippy::type_complexity)] - let (handle, _rx) = tokio::sync::mpsc::channel::<( - Vec, - InvalidationOrigin, - broadcast::Sender>, - )>(128); - let invalidation = Invalidation { handle }; let config = Arc::new(SubgraphConfiguration { all: Subgraph { ttl: None, @@ -402,92 +324,4 @@ mod tests { let res = service.oneshot(req).await.unwrap(); assert_eq!(res.response.status(), StatusCode::UNAUTHORIZED); } - - #[tokio::test] - async fn test_invalidation_service() { - let (handle, mut rx) = tokio::sync::mpsc::channel::<( - Vec, - InvalidationOrigin, - broadcast::Sender>, - )>(128); - let invalidation = Invalidation { handle }; - - tokio::task::spawn(async move { - let mut called = false; - while let Some((requests, origin, response_tx)) = rx.recv().await { - called = true; - if requests - != [ - InvalidationRequest::Subgraph { - subgraph: String::from("test"), - }, - InvalidationRequest::Type { - subgraph: String::from("test"), - r#type: String::from("Test"), - }, - ] - { - response_tx - .send(Err(InvalidationError::Custom(format!( - "it's not the right invalidation requests : {requests:?}" - )))) - .unwrap(); - return; - } - if origin != InvalidationOrigin::Endpoint { - response_tx - .send(Err(InvalidationError::Custom(format!( - "it's not the right invalidation origin : {origin:?}" - )))) - .unwrap(); - return; - } - response_tx.send(Ok(2)).unwrap(); - } - assert!(called); - }); - - let config = Arc::new(SubgraphConfiguration { - all: Subgraph { - ttl: None, - enabled: true, - private_id: None, - redis: None, - invalidation: Some(SubgraphInvalidationConfig { - enabled: true, - shared_key: String::from("test"), - }), - }, - subgraphs: HashMap::new(), - }); - let service = InvalidationService::new(config, invalidation); - let req = router::Request::fake_builder() - .method(http::Method::POST) - .header(AUTHORIZATION, "test") - .body( - serde_json::to_vec(&[ - InvalidationRequest::Subgraph { - subgraph: String::from("test"), - }, - InvalidationRequest::Type { - subgraph: String::from("test"), - r#type: String::from("Test"), - }, - ]) - .unwrap(), - ) - .build() - .unwrap(); - let res = service.oneshot(req).await.unwrap(); - assert_eq!(res.response.status(), StatusCode::ACCEPTED); - assert_eq!( - serde_json::from_slice::( - &hyper::body::to_bytes(res.response.into_body()) - .await - .unwrap() - ) - .unwrap(), - serde_json::json!({"count": 2}) - ); - } } diff --git a/apollo-router/src/plugins/cache/tests.rs b/apollo-router/src/plugins/cache/tests.rs index 36628acf5e..f9e83e3a81 100644 --- a/apollo-router/src/plugins/cache/tests.rs +++ b/apollo-router/src/plugins/cache/tests.rs @@ -76,7 +76,7 @@ pub(crate) struct MockStore { } impl MockStore { - fn new() -> MockStore { + pub(crate) fn new() -> MockStore { MockStore { map: Arc::new(Mutex::new(HashMap::new())), } From 3f03c43f159b8ce1731bcfcda5603f77a4dd7016 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Tue, 10 Sep 2024 14:38:01 +0200 Subject: [PATCH 13/56] Update apollo-rs crates (#5980) --- Cargo.lock | 12 ++++++------ Cargo.toml | 4 ++-- examples/supergraph-sdl/rust/Cargo.toml | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38dbfa5129..ef2db01a17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,9 +159,9 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "apollo-compiler" -version = "1.0.0-beta.21" +version = "1.0.0-beta.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9496debc2b28a7da94aa6329b653fae37e0f0ec44da93d82a8d5d2a6a82abe0e" +checksum = "6feccaf8fab00732a73dd3a40e4aaa7a1106ceef3c0bfbc591c4e0ba27e07774" dependencies = [ "ahash", "apollo-parser", @@ -218,9 +218,9 @@ dependencies = [ [[package]] name = "apollo-parser" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a43dc64e71ca7140e646b99bf86ae721ebb801d2aec44e29a654c4d035ab8" +checksum = "9692c1bfa7e0628e5c46bd0538571dd469138a0f062290fd4bf90e20e46f05da" dependencies = [ "memchr", "rowan", @@ -449,9 +449,9 @@ dependencies = [ [[package]] name = "apollo-smith" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de03c56d7feec663e7f9e981cf4570db68a0901de8c4667f5b5d20321b88af6e" +checksum = "10e88b295221ae9d2af63d5eac86cb1f47ec8449e7440253d7772fc305a6c200" dependencies = [ "apollo-compiler", "apollo-parser", diff --git a/Cargo.toml b/Cargo.toml index af317aa7d7..7672f73804 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,9 +49,9 @@ debug = 1 # Dependencies used in more than one place are specified here in order to keep versions in sync: # https://doc.rust-lang.org/cargo/reference/workspaces.html#the-dependencies-table [workspace.dependencies] -apollo-compiler = "=1.0.0-beta.21" +apollo-compiler = "=1.0.0-beta.22" apollo-parser = "0.8.0" -apollo-smith = "0.11.0" +apollo-smith = "0.12.0" async-trait = "0.1.77" hex = { version = "0.4.3", features = ["serde"] } http = "0.2.11" diff --git a/examples/supergraph-sdl/rust/Cargo.toml b/examples/supergraph-sdl/rust/Cargo.toml index 065990a8ef..97d3d9d67f 100644 --- a/examples/supergraph-sdl/rust/Cargo.toml +++ b/examples/supergraph-sdl/rust/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] anyhow = "1" -apollo-compiler = "=1.0.0-beta.21" +apollo-compiler = "=1.0.0-beta.22" apollo-router = { path = "../../../apollo-router" } async-trait = "0.1" tower = { version = "0.4", features = ["full"] } From d4ab58d29dd79beacd399b8247270f600336ed08 Mon Sep 17 00:00:00 2001 From: Taylor Ninesling Date: Tue, 10 Sep 2024 09:40:51 -0500 Subject: [PATCH 14/56] Move cost context values into DashMap (#5972) --- ...feat_tninesling_coprocessor_cost_access.md | 5 + .../src/plugins/demand_control/mod.rs | 100 +++++++---- .../strategy/static_estimated.rs | 38 ++-- .../plugins/demand_control/strategy/test.rs | 83 +++++---- apollo-router/src/plugins/rhai/engine.rs | 12 ++ apollo-router/src/plugins/rhai/tests.rs | 60 +++++++ .../plugins/telemetry/config_new/cost/mod.rs | 165 ++++++++++-------- .../plugins/telemetry/config_new/selectors.rs | 31 +++- apollo-router/src/plugins/telemetry/mod.rs | 30 +++- apollo-router/src/router_factory.rs | 2 +- .../tests/fixtures/demand_control.rhai | 18 ++ .../tests/integration/coprocessor.rs | 68 ++++++++ .../coprocessor_demand_control.router.yaml | 17 ++ docs/source/customizations/rhai-api.mdx | 4 + 14 files changed, 457 insertions(+), 176 deletions(-) create mode 100644 .changesets/feat_tninesling_coprocessor_cost_access.md create mode 100644 apollo-router/tests/fixtures/demand_control.rhai create mode 100644 apollo-router/tests/integration/fixtures/coprocessor_demand_control.router.yaml diff --git a/.changesets/feat_tninesling_coprocessor_cost_access.md b/.changesets/feat_tninesling_coprocessor_cost_access.md new file mode 100644 index 0000000000..a8c1a7a6f5 --- /dev/null +++ b/.changesets/feat_tninesling_coprocessor_cost_access.md @@ -0,0 +1,5 @@ +### Move cost context values into DashMap ([PR #5972](https://github.com/apollographql/router/pull/5972)) + +Allow Rhai scripts and coprocessors to access demand control information via context. + +By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/5972 diff --git a/apollo-router/src/plugins/demand_control/mod.rs b/apollo-router/src/plugins/demand_control/mod.rs index b3faaef747..0e779cad01 100644 --- a/apollo-router/src/plugins/demand_control/mod.rs +++ b/apollo-router/src/plugins/demand_control/mod.rs @@ -42,36 +42,11 @@ use crate::Context; pub(crate) mod cost_calculator; pub(crate) mod strategy; -/// The cost calculation information stored in context for use in telemetry and other plugins that need to know what cost was calculated. -#[derive(Debug, Clone)] -pub(crate) struct CostContext { - pub(crate) estimated: f64, - pub(crate) actual: f64, - pub(crate) result: &'static str, - pub(crate) strategy: &'static str, -} - -impl Default for CostContext { - fn default() -> Self { - Self { - estimated: 0.0, - actual: 0.0, - result: "COST_OK", - strategy: "COST_STRATEGY_UNKNOWN", - } - } -} - -impl CostContext { - pub(crate) fn delta(&self) -> f64 { - self.estimated - self.actual - } - - pub(crate) fn result(&mut self, error: DemandControlError) -> DemandControlError { - self.result = error.code(); - error - } -} +pub(crate) static COST_ESTIMATED_KEY: &str = "cost.estimated"; +pub(crate) static COST_ACTUAL_KEY: &str = "cost.actual"; +pub(crate) static COST_DELTA_KEY: &str = "cost.delta"; +pub(crate) static COST_RESULT_KEY: &str = "cost.result"; +pub(crate) static COST_STRATEGY_KEY: &str = "cost.strategy"; /// Algorithm for calculating the cost of an incoming query. #[derive(Clone, Debug, Deserialize, JsonSchema)] @@ -146,6 +121,8 @@ pub(crate) enum DemandControlError { QueryParseFailure(String), /// {0} SubgraphOperationNotInitialized(crate::query_planner::fetch::SubgraphOperationNotInitialized), + /// {0} + ContextSerializationError(String), } impl IntoGraphQLErrors for DemandControlError { @@ -182,6 +159,10 @@ impl IntoGraphQLErrors for DemandControlError { .message(self.to_string()) .build()]), DemandControlError::SubgraphOperationNotInitialized(e) => Ok(e.into_graphql_errors()), + DemandControlError::ContextSerializationError(_) => Ok(vec![graphql::Error::builder() + .extension_code(self.code()) + .message(self.to_string()) + .build()]), } } } @@ -193,6 +174,7 @@ impl DemandControlError { DemandControlError::ActualCostTooExpensive { .. } => "COST_ACTUAL_TOO_EXPENSIVE", DemandControlError::QueryParseFailure(_) => "COST_QUERY_PARSE_FAILURE", DemandControlError::SubgraphOperationNotInitialized(e) => e.code(), + DemandControlError::ContextSerializationError(_) => "COST_CONTEXT_SERIALIZATION_ERROR", } } } @@ -219,6 +201,58 @@ impl<'a> From> for DemandControlError { } } +impl Context { + pub(crate) fn insert_estimated_cost(&self, cost: f64) -> Result<(), DemandControlError> { + self.insert(COST_ESTIMATED_KEY, cost) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string()))?; + Ok(()) + } + + pub(crate) fn get_estimated_cost(&self) -> Result, DemandControlError> { + self.get::<&str, f64>(COST_ESTIMATED_KEY) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string())) + } + + pub(crate) fn insert_actual_cost(&self, cost: f64) -> Result<(), DemandControlError> { + self.insert(COST_ACTUAL_KEY, cost) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string()))?; + Ok(()) + } + + pub(crate) fn get_actual_cost(&self) -> Result, DemandControlError> { + self.get::<&str, f64>(COST_ACTUAL_KEY) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string())) + } + + pub(crate) fn get_cost_delta(&self) -> Result, DemandControlError> { + let estimated = self.get_estimated_cost()?; + let actual = self.get_actual_cost()?; + Ok(estimated.zip(actual).map(|(est, act)| est - act)) + } + + pub(crate) fn insert_cost_result(&self, result: String) -> Result<(), DemandControlError> { + self.insert(COST_RESULT_KEY, result) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string()))?; + Ok(()) + } + + pub(crate) fn get_cost_result(&self) -> Result, DemandControlError> { + self.get::<&str, String>(COST_RESULT_KEY) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string())) + } + + pub(crate) fn insert_cost_strategy(&self, strategy: String) -> Result<(), DemandControlError> { + self.insert(COST_STRATEGY_KEY, strategy) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string()))?; + Ok(()) + } + + pub(crate) fn get_cost_strategy(&self) -> Result, DemandControlError> { + self.get::<&str, String>(COST_STRATEGY_KEY) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string())) + } +} + pub(crate) struct DemandControl { config: DemandControlConfig, strategy_factory: StrategyFactory, @@ -227,8 +261,10 @@ pub(crate) struct DemandControl { impl DemandControl { fn report_operation_metric(context: Context) { let result = context - .extensions() - .with_lock(|lock| lock.get::().map_or("NO_CONTEXT", |c| c.result)); + .get(COST_RESULT_KEY) + .ok() + .flatten() + .unwrap_or("NO_CONTEXT".to_string()); u64_counter!( "apollo.router.operations.demand_control", "Total operations with demand control enabled", diff --git a/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs b/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs index 41de6926b4..3ee1894473 100644 --- a/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs +++ b/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs @@ -3,7 +3,6 @@ use apollo_compiler::ExecutableDocument; use crate::graphql; use crate::plugins::demand_control::cost_calculator::static_cost::StaticCostCalculator; use crate::plugins::demand_control::strategy::StrategyImpl; -use crate::plugins::demand_control::CostContext; use crate::plugins::demand_control::DemandControlError; use crate::services::execution; use crate::services::subgraph; @@ -20,21 +19,24 @@ impl StrategyImpl for StaticEstimated { self.cost_calculator .planned(&request.query_plan) .and_then(|cost| { - request.context.extensions().with_lock(|mut lock| { - let cost_result = lock.get_or_default_mut::(); - cost_result.strategy = "static_estimated"; - cost_result.estimated = cost; - if cost > self.max { - Err( - cost_result.result(DemandControlError::EstimatedCostTooExpensive { - estimated_cost: cost, - max_cost: self.max, - }), - ) - } else { - Ok(()) - } - }) + request + .context + .insert_cost_strategy("static_estimated".to_string())?; + request.context.insert_cost_result("COST_OK".to_string())?; + request.context.insert_estimated_cost(cost)?; + + if cost > self.max { + let error = DemandControlError::EstimatedCostTooExpensive { + estimated_cost: cost, + max_cost: self.max, + }; + request + .context + .insert_cost_result(error.code().to_string())?; + Err(error) + } else { + Ok(()) + } }) } @@ -58,9 +60,7 @@ impl StrategyImpl for StaticEstimated { ) -> Result<(), DemandControlError> { if response.data.is_some() { let cost = self.cost_calculator.actual(request, response)?; - context - .extensions() - .with_lock(|mut lock| lock.get_or_default_mut::().actual = cost); + context.insert_actual_cost(cost)?; } Ok(()) } diff --git a/apollo-router/src/plugins/demand_control/strategy/test.rs b/apollo-router/src/plugins/demand_control/strategy/test.rs index 347f77d79f..265613472d 100644 --- a/apollo-router/src/plugins/demand_control/strategy/test.rs +++ b/apollo-router/src/plugins/demand_control/strategy/test.rs @@ -3,7 +3,6 @@ use apollo_compiler::ExecutableDocument; use crate::plugins::demand_control::strategy::StrategyImpl; use crate::plugins::demand_control::test::TestError; use crate::plugins::demand_control::test::TestStage; -use crate::plugins::demand_control::CostContext; use crate::plugins::demand_control::DemandControlError; use crate::services::execution::Request; use crate::services::subgraph::Response; @@ -17,32 +16,38 @@ pub(crate) struct Test { impl StrategyImpl for Test { fn on_execution_request(&self, request: &Request) -> Result<(), DemandControlError> { - request.context.extensions().with_lock(|mut lock| { - let cost_context = lock.get_or_default_mut::(); - match self { - Test { - stage: TestStage::ExecutionRequest, - error, - } => Err(cost_context.result(error.into())), - _ => Ok(()), + match self { + Test { + stage: TestStage::ExecutionRequest, + error, + } => { + let error: DemandControlError = error.into(); + request + .context + .insert_cost_result(error.code().to_string())?; + Err(error) } - }) + _ => Ok(()), + } } fn on_subgraph_request( &self, request: &crate::services::subgraph::Request, ) -> Result<(), DemandControlError> { - request.context.extensions().with_lock(|mut lock| { - let cost_context = lock.get_or_default_mut::(); - match self { - Test { - stage: TestStage::SubgraphRequest, - error, - } => Err(cost_context.result(error.into())), - _ => Ok(()), + match self { + Test { + stage: TestStage::SubgraphRequest, + error, + } => { + let error: DemandControlError = error.into(); + request + .context + .insert_cost_result(error.code().to_string())?; + Err(error) } - }) + _ => Ok(()), + } } fn on_subgraph_response( @@ -50,16 +55,19 @@ impl StrategyImpl for Test { _request: &ExecutableDocument, response: &Response, ) -> Result<(), DemandControlError> { - response.context.extensions().with_lock(|mut lock| { - let cost_context = lock.get_or_default_mut::(); - match self { - Test { - stage: TestStage::SubgraphResponse, - error, - } => Err(cost_context.result(error.into())), - _ => Ok(()), + match self { + Test { + stage: TestStage::SubgraphResponse, + error, + } => { + let error: DemandControlError = error.into(); + response + .context + .insert_cost_result(error.code().to_string())?; + Err(error) } - }) + _ => Ok(()), + } } fn on_execution_response( @@ -68,15 +76,16 @@ impl StrategyImpl for Test { _request: &ExecutableDocument, _response: &crate::graphql::Response, ) -> Result<(), DemandControlError> { - context.extensions().with_lock(|mut lock| { - let cost_context = lock.get_or_default_mut::(); - match self { - Test { - stage: TestStage::ExecutionResponse, - error, - } => Err(cost_context.result(error.into())), - _ => Ok(()), + match self { + Test { + stage: TestStage::ExecutionResponse, + error, + } => { + let error: DemandControlError = error.into(); + context.insert_cost_result(error.code().to_string())?; + Err(error) } - }) + _ => Ok(()), + } } } diff --git a/apollo-router/src/plugins/rhai/engine.rs b/apollo-router/src/plugins/rhai/engine.rs index da87655c17..062143711f 100644 --- a/apollo-router/src/plugins/rhai/engine.rs +++ b/apollo-router/src/plugins/rhai/engine.rs @@ -46,6 +46,10 @@ use crate::graphql::Response; use crate::http_ext; use crate::plugins::authentication::APOLLO_AUTHENTICATION_JWT_CLAIMS; use crate::plugins::cache::entity::CONTEXT_CACHE_KEY; +use crate::plugins::demand_control::COST_ACTUAL_KEY; +use crate::plugins::demand_control::COST_ESTIMATED_KEY; +use crate::plugins::demand_control::COST_RESULT_KEY; +use crate::plugins::demand_control::COST_STRATEGY_KEY; use crate::plugins::subscription::SUBSCRIPTION_WS_CUSTOM_CONNECTION_PARAMS; use crate::query_planner::APOLLO_OPERATION_ID; use crate::Context; @@ -1777,6 +1781,14 @@ impl Rhai { ); global_variables.insert("APOLLO_ENTITY_CACHE_KEY".into(), CONTEXT_CACHE_KEY.into()); global_variables.insert("APOLLO_OPERATION_ID".into(), APOLLO_OPERATION_ID.into()); + // Demand Control Context Keys + global_variables.insert( + "APOLLO_COST_ESTIMATED_KEY".into(), + COST_ESTIMATED_KEY.into(), + ); + global_variables.insert("APOLLO_COST_ACTUAL_KEY".into(), COST_ACTUAL_KEY.into()); + global_variables.insert("APOLLO_COST_STRATEGY_KEY".into(), COST_STRATEGY_KEY.into()); + global_variables.insert("APOLLO_COST_RESULT_KEY".into(), COST_RESULT_KEY.into()); let shared_globals = Arc::new(global_variables); diff --git a/apollo-router/src/plugins/rhai/tests.rs b/apollo-router/src/plugins/rhai/tests.rs index b47c25774d..11c4f3a03a 100644 --- a/apollo-router/src/plugins/rhai/tests.rs +++ b/apollo-router/src/plugins/rhai/tests.rs @@ -785,3 +785,63 @@ async fn test_router_service_adds_timestamp_header() -> Result<(), BoxError> { Ok(()) } + +#[tokio::test] +async fn it_can_access_demand_control_context() -> Result<(), BoxError> { + let mut mock_service = MockSupergraphService::new(); + mock_service + .expect_call() + .times(1) + .returning(move |req: SupergraphRequest| { + Ok(SupergraphResponse::fake_builder() + .context(req.context) + .build() + .unwrap()) + }); + + let dyn_plugin: Box = crate::plugin::plugins() + .find(|factory| factory.name == "apollo.rhai") + .expect("Plugin not found") + .create_instance_without_schema( + &Value::from_str(r#"{"scripts":"tests/fixtures", "main":"demand_control.rhai"}"#) + .unwrap(), + ) + .await + .unwrap(); + + let mut router_service = dyn_plugin.supergraph_service(BoxService::new(mock_service)); + let context = Context::new(); + context.insert_estimated_cost(50.0).unwrap(); + context.insert_actual_cost(35.0).unwrap(); + context + .insert_cost_strategy("test_strategy".to_string()) + .unwrap(); + context.insert_cost_result("COST_OK".to_string()).unwrap(); + let supergraph_req = SupergraphRequest::fake_builder().context(context).build()?; + + let service_response = router_service.ready().await?.call(supergraph_req).await?; + assert_eq!(StatusCode::OK, service_response.response.status()); + + let headers = service_response.response.headers().clone(); + let demand_control_header = headers + .get("demand-control-estimate") + .map(|h| h.to_str().unwrap()); + assert_eq!(demand_control_header, Some("50.0")); + + let demand_control_header = headers + .get("demand-control-actual") + .map(|h| h.to_str().unwrap()); + assert_eq!(demand_control_header, Some("35.0")); + + let demand_control_header = headers + .get("demand-control-strategy") + .map(|h| h.to_str().unwrap()); + assert_eq!(demand_control_header, Some("test_strategy")); + + let demand_control_header = headers + .get("demand-control-result") + .map(|h| h.to_str().unwrap()); + assert_eq!(demand_control_header, Some("COST_OK")); + + Ok(()) +} diff --git a/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs b/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs index 31693b6a31..8341790eac 100644 --- a/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs +++ b/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs @@ -13,7 +13,10 @@ use super::attributes::StandardAttribute; use super::instruments::Increment; use super::instruments::StaticInstrument; use crate::metrics; -use crate::plugins::demand_control::CostContext; +use crate::plugins::demand_control::COST_ACTUAL_KEY; +use crate::plugins::demand_control::COST_DELTA_KEY; +use crate::plugins::demand_control::COST_ESTIMATED_KEY; +use crate::plugins::demand_control::COST_RESULT_KEY; use crate::plugins::telemetry::config::AttributeValue; use crate::plugins::telemetry::config_new::attributes::SupergraphAttributes; use crate::plugins::telemetry::config_new::conditions::Condition; @@ -38,10 +41,6 @@ pub(crate) const APOLLO_PRIVATE_COST_STRATEGY: Key = pub(crate) const APOLLO_PRIVATE_COST_RESULT: Key = Key::from_static_str("apollo_private.cost.result"); -static COST_ESTIMATED: &str = "cost.estimated"; -static COST_ACTUAL: &str = "cost.actual"; -static COST_DELTA: &str = "cost.delta"; - /// Attributes for Cost #[derive(Deserialize, JsonSchema, Clone, Default, Debug, PartialEq)] #[serde(deny_unknown_fields, default)] @@ -79,43 +78,60 @@ impl Selectors for SupergraphCostAttributes { fn on_response_event(&self, _response: &Self::EventResponse, ctx: &Context) -> Vec { let mut attrs = Vec::with_capacity(4); - let cost_result = ctx - .extensions() - .with_lock(|lock| lock.get::().cloned()); - if let Some(cost_result) = cost_result { - if let Some(key) = self - .cost_estimated - .as_ref() - .and_then(|a| a.key(Key::from_static_str("cost.estimated"))) - { - attrs.push(KeyValue::new(key, cost_result.estimated)); - } - if let Some(key) = self - .cost_actual - .as_ref() - .and_then(|a| a.key(Key::from_static_str("cost.actual"))) - { - attrs.push(KeyValue::new(key, cost_result.actual)); - } - if let Some(key) = self - .cost_delta - .as_ref() - .and_then(|a| a.key(Key::from_static_str("cost.delta"))) - { - attrs.push(KeyValue::new(key, cost_result.delta())); - } - if let Some(key) = self - .cost_result - .as_ref() - .and_then(|a| a.key(Key::from_static_str("cost.result"))) - { - attrs.push(KeyValue::new(key, cost_result.result)); - } + if let Some(estimated_cost) = self.estimated_cost_if_configured(ctx) { + attrs.push(estimated_cost); + } + if let Some(actual_cost) = self.actual_cost_if_configured(ctx) { + attrs.push(actual_cost); + } + if let Some(cost_delta) = self.cost_delta_if_configured(ctx) { + attrs.push(cost_delta); + } + if let Some(cost_result) = self.cost_result_if_configured(ctx) { + attrs.push(cost_result); } attrs } } +impl SupergraphCostAttributes { + fn estimated_cost_if_configured(&self, ctx: &Context) -> Option { + let key = self + .cost_estimated + .as_ref()? + .key(Key::from_static_str(COST_ESTIMATED_KEY))?; + let value = ctx.get_estimated_cost().ok()??; + Some(KeyValue::new(key, value)) + } + + fn actual_cost_if_configured(&self, ctx: &Context) -> Option { + let key = self + .cost_actual + .as_ref()? + .key(Key::from_static_str(COST_ACTUAL_KEY))?; + let value = ctx.get_actual_cost().ok()??; + Some(KeyValue::new(key, value)) + } + + fn cost_delta_if_configured(&self, ctx: &Context) -> Option { + let key = self + .cost_delta + .as_ref()? + .key(Key::from_static_str("cost.delta"))?; + let value = ctx.get_cost_delta().ok()??; + Some(KeyValue::new(key, value)) + } + + fn cost_result_if_configured(&self, ctx: &Context) -> Option { + let key = self + .cost_result + .as_ref()? + .key(Key::from_static_str(COST_RESULT_KEY))?; + let value = ctx.get_cost_result().ok()??; + Some(KeyValue::new(key, value)) + } +} + #[derive(Deserialize, JsonSchema, Clone, Default, Debug)] #[serde(deny_unknown_fields, default)] pub(crate) struct CostInstrumentsConfig { @@ -139,14 +155,14 @@ impl CostInstrumentsConfig { .meter(crate::plugins::telemetry::config_new::instruments::METER_NAME); [( - COST_ESTIMATED.to_string(), - StaticInstrument::Histogram(meter.f64_histogram(COST_ESTIMATED).with_description("Estimated cost of the operation using the currently configured cost model").init()), + COST_ESTIMATED_KEY.to_string(), + StaticInstrument::Histogram(meter.f64_histogram(COST_ESTIMATED_KEY).with_description("Estimated cost of the operation using the currently configured cost model").init()), ),( - COST_ACTUAL.to_string(), - StaticInstrument::Histogram(meter.f64_histogram(COST_ACTUAL).with_description("Actual cost of the operation using the currently configured cost model").init()), + COST_ACTUAL_KEY.to_string(), + StaticInstrument::Histogram(meter.f64_histogram(COST_ACTUAL_KEY).with_description("Actual cost of the operation using the currently configured cost model").init()), ),( - COST_DELTA.to_string(), - StaticInstrument::Histogram(meter.f64_histogram(COST_DELTA).with_description("Delta between the estimated and actual cost of the operation using the currently configured cost model").init()), + COST_DELTA_KEY.to_string(), + StaticInstrument::Histogram(meter.f64_histogram(COST_DELTA_KEY).with_description("Delta between the estimated and actual cost of the operation using the currently configured cost model").init()), )] .into_iter() .collect() @@ -158,7 +174,7 @@ impl CostInstrumentsConfig { ) -> CostInstruments { let cost_estimated = self.cost_estimated.is_enabled().then(|| { Self::histogram( - COST_ESTIMATED, + COST_ESTIMATED_KEY, &self.cost_estimated, SupergraphSelector::Cost { cost: CostValue::Estimated, @@ -169,7 +185,7 @@ impl CostInstrumentsConfig { let cost_actual = self.cost_actual.is_enabled().then(|| { Self::histogram( - COST_ACTUAL, + COST_ACTUAL_KEY, &self.cost_actual, SupergraphSelector::Cost { cost: CostValue::Actual, @@ -180,7 +196,7 @@ impl CostInstrumentsConfig { let cost_delta = self.cost_delta.is_enabled().then(|| { Self::histogram( - COST_DELTA, + COST_DELTA_KEY, &self.cost_delta, SupergraphSelector::Cost { cost: CostValue::Delta, @@ -331,26 +347,30 @@ pub(crate) enum CostValue { } pub(crate) fn add_cost_attributes(context: &Context, custom_attributes: &mut Vec) { - context.extensions().with_lock(|c| { - if let Some(cost) = c.get::() { - custom_attributes.push(KeyValue::new( - APOLLO_PRIVATE_COST_ESTIMATED.clone(), - AttributeValue::F64(cost.estimated), - )); - custom_attributes.push(KeyValue::new( - APOLLO_PRIVATE_COST_ACTUAL.clone(), - AttributeValue::F64(cost.actual), - )); - custom_attributes.push(KeyValue::new( - APOLLO_PRIVATE_COST_RESULT.clone(), - AttributeValue::String(cost.result.into()), - )); - custom_attributes.push(KeyValue::new( - APOLLO_PRIVATE_COST_STRATEGY.clone(), - AttributeValue::String(cost.strategy.into()), - )); - } - }); + if let Ok(Some(cost)) = context.get_estimated_cost() { + custom_attributes.push(KeyValue::new( + APOLLO_PRIVATE_COST_ESTIMATED.clone(), + AttributeValue::F64(cost), + )); + } + if let Ok(Some(cost)) = context.get_actual_cost() { + custom_attributes.push(KeyValue::new( + APOLLO_PRIVATE_COST_ACTUAL.clone(), + AttributeValue::F64(cost), + )); + } + if let Ok(Some(result)) = context.get_cost_result() { + custom_attributes.push(KeyValue::new( + APOLLO_PRIVATE_COST_RESULT.clone(), + AttributeValue::String(result), + )); + } + if let Ok(Some(strategy)) = context.get_cost_strategy() { + custom_attributes.push(KeyValue::new( + APOLLO_PRIVATE_COST_STRATEGY.clone(), + AttributeValue::String(strategy), + )); + } } #[cfg(test)] @@ -358,7 +378,6 @@ mod test { use std::sync::Arc; use crate::context::OPERATION_NAME; - use crate::plugins::demand_control::CostContext; use crate::plugins::telemetry::config_new::cost::CostInstruments; use crate::plugins::telemetry::config_new::cost::CostInstrumentsConfig; use crate::plugins::telemetry::config_new::instruments::Instrumented; @@ -463,13 +482,11 @@ mod test { fn make_request(instruments: &CostInstruments) { let context = Context::new(); - context.extensions().with_lock(|mut lock| { - lock.insert(CostContext::default()); - let cost_result = lock.get_or_default_mut::(); - cost_result.estimated = 100.0; - cost_result.actual = 10.0; - cost_result.result = "COST_TOO_EXPENSIVE" - }); + context.insert_estimated_cost(100.0).unwrap(); + context.insert_actual_cost(10.0).unwrap(); + context + .insert_cost_result("COST_TOO_EXPENSIVE".to_string()) + .unwrap(); let _ = context.insert(OPERATION_NAME, "Test".to_string()).unwrap(); instruments.on_request( &supergraph::Request::fake_builder() diff --git a/apollo-router/src/plugins/telemetry/config_new/selectors.rs b/apollo-router/src/plugins/telemetry/config_new/selectors.rs index 9047764a80..8c9c8dde7a 100644 --- a/apollo-router/src/plugins/telemetry/config_new/selectors.rs +++ b/apollo-router/src/plugins/telemetry/config_new/selectors.rs @@ -14,7 +14,6 @@ use crate::plugin::serde::deserialize_json_query; use crate::plugin::serde::deserialize_jsonpath; use crate::plugins::cache::entity::CacheSubgraph; use crate::plugins::cache::metrics::CacheMetricContextKey; -use crate::plugins::demand_control::CostContext; use crate::plugins::telemetry::config::AttributeValue; use crate::plugins::telemetry::config::TraceIdFormat; use crate::plugins::telemetry::config_new::cost::CostValue; @@ -1090,14 +1089,28 @@ impl Selector for SupergraphSelector { val.maybe_to_otel_value() } .or_else(|| default.maybe_to_otel_value()), - SupergraphSelector::Cost { cost } => ctx.extensions().with_lock(|lock| { - lock.get::().map(|cost_result| match cost { - CostValue::Estimated => cost_result.estimated.into(), - CostValue::Actual => cost_result.actual.into(), - CostValue::Delta => cost_result.delta().into(), - CostValue::Result => cost_result.result.into(), - }) - }), + SupergraphSelector::Cost { cost } => match cost { + CostValue::Estimated => ctx + .get_estimated_cost() + .ok() + .flatten() + .map(opentelemetry::Value::from), + CostValue::Actual => ctx + .get_actual_cost() + .ok() + .flatten() + .map(opentelemetry::Value::from), + CostValue::Delta => ctx + .get_cost_delta() + .ok() + .flatten() + .map(opentelemetry::Value::from), + CostValue::Result => ctx + .get_cost_result() + .ok() + .flatten() + .map(opentelemetry::Value::from), + }, SupergraphSelector::OnGraphQLError { on_graphql_error } if *on_graphql_error => { if ctx.get_json_value(CONTAINS_GRAPHQL_ERROR) == Some(serde_json_bytes::Value::Bool(true)) diff --git a/apollo-router/src/plugins/telemetry/mod.rs b/apollo-router/src/plugins/telemetry/mod.rs index 7fdc8cf496..886bebea1a 100644 --- a/apollo-router/src/plugins/telemetry/mod.rs +++ b/apollo-router/src/plugins/telemetry/mod.rs @@ -1516,12 +1516,11 @@ impl Telemetry { let root_error_stats = Self::per_path_error_stats(&traces); let limits_stats = context.extensions().with_lock(|guard| { let strategy = guard.get::(); - let cost_ctx = guard.get::(); let query_limits = guard.get::>(); SingleLimitsStats { strategy: strategy.and_then(|s| serde_json::to_string(&s.mode).ok()), - cost_estimated: cost_ctx.map(|ctx| ctx.estimated), - cost_actual: cost_ctx.map(|ctx| ctx.actual), + cost_estimated: context.get_estimated_cost().ok().flatten(), + cost_actual: context.get_actual_cost().ok().flatten(), // These limits are related to the Traffic Shaping feature, unrelated to the Demand Control plugin depth: query_limits.map_or(0, |ql| ql.depth as u64), @@ -2176,8 +2175,11 @@ mod tests { use crate::plugin::test::MockSubgraphService; use crate::plugin::test::MockSupergraphService; use crate::plugin::DynPlugin; - use crate::plugins::demand_control::CostContext; use crate::plugins::demand_control::DemandControlError; + use crate::plugins::demand_control::COST_ACTUAL_KEY; + use crate::plugins::demand_control::COST_ESTIMATED_KEY; + use crate::plugins::demand_control::COST_RESULT_KEY; + use crate::plugins::demand_control::COST_STRATEGY_KEY; use crate::plugins::telemetry::config::TraceIdFormat; use crate::plugins::telemetry::handle_error_internal; use crate::services::router::body::get_body_bytes; @@ -3249,6 +3251,14 @@ mod tests { ); } + #[derive(Clone)] + struct CostContext { + pub(crate) estimated: f64, + pub(crate) actual: f64, + pub(crate) result: &'static str, + pub(crate) strategy: &'static str, + } + async fn make_failed_demand_control_request(plugin: &dyn DynPlugin, cost_details: CostContext) { let mut mock_service = MockSupergraphService::new(); mock_service @@ -3258,6 +3268,18 @@ mod tests { req.context.extensions().with_lock(|mut lock| { lock.insert(cost_details.clone()); }); + req.context + .insert(COST_ESTIMATED_KEY, cost_details.estimated) + .unwrap(); + req.context + .insert(COST_ACTUAL_KEY, cost_details.actual) + .unwrap(); + req.context + .insert(COST_RESULT_KEY, cost_details.result.to_string()) + .unwrap(); + req.context + .insert(COST_STRATEGY_KEY, cost_details.strategy.to_string()) + .unwrap(); let errors = if cost_details.result == "COST_ESTIMATED_TOO_EXPENSIVE" { DemandControlError::EstimatedCostTooExpensive { diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs index 640cbc9aa7..bb4c4a1522 100644 --- a/apollo-router/src/router_factory.rs +++ b/apollo-router/src/router_factory.rs @@ -682,11 +682,11 @@ pub(crate) async fn create_plugins( add_optional_apollo_plugin!("preview_file_uploads"); add_optional_apollo_plugin!("preview_entity_cache"); add_mandatory_apollo_plugin!("progressive_override"); + add_optional_apollo_plugin!("demand_control"); // This relative ordering is documented in `docs/source/customizations/native.mdx`: add_optional_apollo_plugin!("rhai"); add_optional_apollo_plugin!("coprocessor"); - add_optional_apollo_plugin!("demand_control"); add_user_plugins!(); // Macros above remove from `apollo_plugin_factories`, so anything left at the end diff --git a/apollo-router/tests/fixtures/demand_control.rhai b/apollo-router/tests/fixtures/demand_control.rhai new file mode 100644 index 0000000000..8232b1ad28 --- /dev/null +++ b/apollo-router/tests/fixtures/demand_control.rhai @@ -0,0 +1,18 @@ +fn supergraph_service(service) { + const response_callback = Fn("process_response"); + service.map_response(response_callback); +} + +fn process_response(response) { + const estimate = response.context[Router.APOLLO_COST_ESTIMATED_KEY]; + response.headers["demand-control-estimate"] = to_string(estimate); + + const actual = response.context[Router.APOLLO_COST_ACTUAL_KEY]; + response.headers["demand-control-actual"] = to_string(actual); + + const strategy = response.context[Router.APOLLO_COST_STRATEGY_KEY]; + response.headers["demand-control-strategy"] = strategy; + + const result = response.context[Router.APOLLO_COST_RESULT_KEY]; + response.headers["demand-control-result"] = result; +} \ No newline at end of file diff --git a/apollo-router/tests/integration/coprocessor.rs b/apollo-router/tests/integration/coprocessor.rs index 213947eae4..d1492fbc87 100644 --- a/apollo-router/tests/integration/coprocessor.rs +++ b/apollo-router/tests/integration/coprocessor.rs @@ -82,3 +82,71 @@ async fn test_coprocessor_limit_payload() -> Result<(), BoxError> { router.graceful_shutdown().await; Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn test_coprocessor_demand_control_access() -> Result<(), BoxError> { + if !graph_os_enabled() { + return Ok(()); + } + + let mock_server = wiremock::MockServer::start().await; + let coprocessor_address = mock_server.uri(); + + // Assert the execution request stage has access to the estimated cost + Mock::given(method("POST")) + .and(path("/")) + .and(body_partial_json(json!({ + "stage": "ExecutionRequest", + "context": { + "entries": { + "cost.estimated": 10.0, + "cost.result": "COST_OK", + "cost.strategy": "static_estimated" + }}}))) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "version":1, + "stage":"ExecutionRequest", + "control":"continue", + }))) + .expect(1) + .mount(&mock_server) + .await; + + // Assert the supergraph response stage also includes the actual cost + Mock::given(method("POST")) + .and(path("/")) + .and(body_partial_json(json!({ + "stage": "SupergraphResponse", + "context": {"entries": { + "cost.actual": 3.0, + "cost.estimated": 10.0, + "cost.result": "COST_OK", + "cost.strategy": "static_estimated" + }}}))) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "version":1, + "stage":"SupergraphResponse", + "control":"continue", + }))) + .expect(1) + .mount(&mock_server) + .await; + + let mut router = IntegrationTest::builder() + .config( + include_str!("fixtures/coprocessor_demand_control.router.yaml") + .replace("", &coprocessor_address), + ) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let (_trace_id, response) = router.execute_default_query().await; + assert_eq!(response.status(), 200); + + router.graceful_shutdown().await; + + Ok(()) +} diff --git a/apollo-router/tests/integration/fixtures/coprocessor_demand_control.router.yaml b/apollo-router/tests/integration/fixtures/coprocessor_demand_control.router.yaml new file mode 100644 index 0000000000..6535dd92bf --- /dev/null +++ b/apollo-router/tests/integration/fixtures/coprocessor_demand_control.router.yaml @@ -0,0 +1,17 @@ +# This coprocessor url will be updated to a test-scoped mock server +coprocessor: + url: "" + execution: + request: + context: true + supergraph: + response: + context: true + +demand_control: + enabled: true + mode: measure + strategy: + static_estimated: + list_size: 10 + max: 1000 \ No newline at end of file diff --git a/docs/source/customizations/rhai-api.mdx b/docs/source/customizations/rhai-api.mdx index c4e4a316f6..f0c0959d00 100644 --- a/docs/source/customizations/rhai-api.mdx +++ b/docs/source/customizations/rhai-api.mdx @@ -359,6 +359,10 @@ Router.APOLLO_AUTHENTICATION_JWT_CLAIMS // Context key to access authentication Router.APOLLO_SUBSCRIPTION_WS_CUSTOM_CONNECTION_PARAMS // Context key to modify or access the custom connection params when using subscriptions in WebSocket to subgraphs (cf subscription docs) Router.APOLLO_ENTITY_CACHE_KEY // Context key to access the entity cache key Router.APOLLO_OPERATION_ID // Context key to get the value of apollo operation id (studio trace id) from the context +Router.APOLLO_COST_ESTIMATED_KEY // Context key to get the estimated cost of an operation +Router.APOLLO_COST_ACTUAL_KEY // Context key to get the actual cost of an operation +Router.APOLLO_COST_STRATEGY_KEY // Context key to get the strategy used to calculate cost +Router.APOLLO_COST_RESULT_KEY // Context key to get the cost result of an operation ``` ## `Request` interface From 4a6640f05adb23b5999c731d0c83a4df2a50246d Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 11 Sep 2024 15:13:41 +0200 Subject: [PATCH 15/56] Create valid documents with authorization filtering (#5952) --- ..._unused_fragments_from_filtered_queries.md | 5 + .../plugins/authorization/authenticated.rs | 350 +++++++++- .../src/plugins/authorization/mod.rs | 3 - .../src/plugins/authorization/policy.rs | 55 +- .../src/plugins/authorization/scopes.rs | 54 +- ...ragment_with_authenticated_root_query.snap | 48 ++ ...ction_mixed_with_authenticated_fields.snap | 14 + ...fragment_nested_in_authenticated_type.snap | 19 + ..._named_fragment_in_authenticated_type.snap | 43 ++ ...__policy__tests__interface_fragment-2.snap | 10 +- ...__scopes__tests__interface_fragment-2.snap | 10 +- ...__scopes__tests__interface_fragment-3.snap | 12 +- .../src/query_planner/bridge_query_planner.rs | 180 ++--- apollo-router/src/query_planner/labeler.rs | 7 + ..._transform__tests__remove_directive-2.snap | 14 + ..._transform__tests__remove_directive-3.snap | 18 + ..._transform__tests__remove_directive-4.snap | 18 + ..._transform__tests__remove_directive-5.snap | 23 + ..._transform__tests__remove_directive-6.snap | 23 + ...y__transform__tests__remove_directive.snap | 20 + apollo-router/src/spec/query/transform.rs | 618 ++++++++++++++++-- 21 files changed, 1302 insertions(+), 242 deletions(-) create mode 100644 .changesets/fix_geal_remove_unused_fragments_from_filtered_queries.md create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__introspection_fragment_with_authenticated_root_query.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__introspection_mixed_with_authenticated_fields.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__named_fragment_nested_in_authenticated_type.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__named_fragment_nested_in_named_fragment_in_authenticated_type.snap create mode 100644 apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-2.snap create mode 100644 apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-3.snap create mode 100644 apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-4.snap create mode 100644 apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-5.snap create mode 100644 apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-6.snap create mode 100644 apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive.snap diff --git a/.changesets/fix_geal_remove_unused_fragments_from_filtered_queries.md b/.changesets/fix_geal_remove_unused_fragments_from_filtered_queries.md new file mode 100644 index 0000000000..772675e6b3 --- /dev/null +++ b/.changesets/fix_geal_remove_unused_fragments_from_filtered_queries.md @@ -0,0 +1,5 @@ +### Create valid documents with authorization filtering ([PR #5952](https://github.com/apollographql/router/pull/5952)) + +This fixes the authorization plugin's query filtering to remove unused fragments and input arguments if the related parts of the query are removed. This was generating validation errors when the query went into query planning + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/5952 \ No newline at end of file diff --git a/apollo-router/src/plugins/authorization/authenticated.rs b/apollo-router/src/plugins/authorization/authenticated.rs index ffe881877b..15bcd7a969 100644 --- a/apollo-router/src/plugins/authorization/authenticated.rs +++ b/apollo-router/src/plugins/authorization/authenticated.rs @@ -13,6 +13,7 @@ use tower::BoxError; use crate::json_ext::Path; use crate::json_ext::PathElement; use crate::spec::query::transform; +use crate::spec::query::transform::TransformState; use crate::spec::query::traverse; use crate::spec::Schema; use crate::spec::TYPENAME; @@ -175,13 +176,13 @@ impl<'a> traverse::Visitor for AuthenticatedCheckVisitor<'a> { pub(crate) struct AuthenticatedVisitor<'a> { schema: &'a schema::Schema, - fragments: HashMap<&'a Name, &'a ast::FragmentDefinition>, + state: TransformState, implementers_map: &'a apollo_compiler::collections::HashMap, pub(crate) query_requires_authentication: bool, pub(crate) unauthorized_paths: Vec, // store the error paths from fragments so we can add them at // the point of application - fragments_unauthorized_paths: HashMap<&'a Name, Vec>, + fragments_unauthorized_paths: HashMap>, current_path: Path, authenticated_directive_name: String, dry_run: bool, @@ -190,13 +191,12 @@ pub(crate) struct AuthenticatedVisitor<'a> { impl<'a> AuthenticatedVisitor<'a> { pub(crate) fn new( schema: &'a schema::Schema, - executable: &'a ast::Document, implementers_map: &'a apollo_compiler::collections::HashMap, dry_run: bool, ) -> Option { Some(Self { schema, - fragments: transform::collect_fragments(executable), + state: TransformState::new(), implementers_map, dry_run, query_requires_authentication: false, @@ -409,17 +409,11 @@ impl<'a> transform::Visitor for AuthenticatedVisitor<'a> { }; if self.unauthorized_paths.len() > current_unauthorized_paths_index { - if let Some((name, _)) = self.fragments.get_key_value(&node.name) { - self.fragments_unauthorized_paths.insert( - name, - self.unauthorized_paths - .split_off(current_unauthorized_paths_index), - ); - } - } - - if let Ok(None) = res { - self.fragments.remove(&node.name); + self.fragments_unauthorized_paths.insert( + node.name.as_str().to_string(), + self.unauthorized_paths + .split_off(current_unauthorized_paths_index), + ); } res @@ -430,29 +424,35 @@ impl<'a> transform::Visitor for AuthenticatedVisitor<'a> { node: &ast::FragmentSpread, ) -> Result, BoxError> { // record the fragment errors at the point of application - if let Some(paths) = self.fragments_unauthorized_paths.get(&node.fragment_name) { + if let Some(paths) = self + .fragments_unauthorized_paths + .get(node.fragment_name.as_str()) + { for path in paths { let path = self.current_path.join(path); self.unauthorized_paths.push(path); } } - let fragment = match self.fragments.get(&node.fragment_name) { - Some(fragment) => fragment, + let condition = match self + .state() + .fragments() + .get(node.fragment_name.as_str()) + .map(|fragment| fragment.fragment.type_condition.clone()) + { + Some(condition) => condition, None => return Ok(None), }; - let condition = &fragment.type_condition; - - self.current_path - .push(PathElement::Fragment(condition.as_str().into())); - let fragment_requires_authentication = self .schema .types - .get(condition) + .get(condition.as_str()) .is_some_and(|type_definition| self.is_type_authenticated(type_definition)); + self.current_path + .push(PathElement::Fragment(condition.as_str().into())); + let res = if fragment_requires_authentication { self.query_requires_authentication = true; self.unauthorized_paths.push(self.current_path.clone()); @@ -515,6 +515,10 @@ impl<'a> transform::Visitor for AuthenticatedVisitor<'a> { fn schema(&self) -> &apollo_compiler::Schema { self.schema } + + fn state(&mut self) -> &mut TransformState { + &mut self.state + } } #[cfg(test)] @@ -609,7 +613,7 @@ mod tests { let doc = ast::Document::parse(query, "query.graphql").unwrap(); let map = schema.implementers_map(); - let mut visitor = AuthenticatedVisitor::new(&schema, &doc, &map, false).unwrap(); + let mut visitor = AuthenticatedVisitor::new(&schema, &map, false).unwrap(); ( transform::document(&mut visitor, &doc).unwrap(), @@ -1689,4 +1693,300 @@ mod tests { assert!(response.next_response().await.is_none()); } + + static AUTHENTICATED_ROOT_TYPE_SCHEMA: &str = r#" + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/authenticated/v0.1", for: SECURITY) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean + ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + directive @join__graph(name: String!, url: String!) on ENUM_VALUE + directive @join__implements( + graph: join__Graph! + interface: String! + ) repeatable on OBJECT | INTERFACE + directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false + ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + + scalar join__FieldSet + 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 + } + + enum join__Graph { + USER @join__graph(name: "user", url: "http://localhost:4001/graphql") + ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") + } + + type Query @join__type(graph: USER) @authenticated { + t: T @join__field(graph: USER) + } + + type T @join__type(graph: USER) { + f: String @join__field(graph: USER) + } + "#; + + #[test] + fn named_fragment_nested_in_authenticated_type() { + static QUERY: &str = r#" + query { + t { + ... F + } + } + + fragment F on T { + f + } + "#; + + let (doc, paths) = filter(AUTHENTICATED_ROOT_TYPE_SCHEMA, QUERY); + + insta::assert_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); + } + + static AUTHENTICATED_TYPE_SCHEMA: &str = r#" + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/authenticated/v0.1", for: SECURITY) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean + ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + directive @join__graph(name: String!, url: String!) on ENUM_VALUE + directive @join__implements( + graph: join__Graph! + interface: String! + ) repeatable on OBJECT | INTERFACE + directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false + ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + + scalar join__FieldSet + 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 + } + + enum join__Graph { + USER @join__graph(name: "user", url: "http://localhost:4001/graphql") + ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") + } + + type Query @join__type(graph: USER){ + t: T @join__field(graph: USER) + u(u:Int): String @join__field(graph: USER) + v(v:Int): Int @join__field(graph: USER) + } + + type T @join__type(graph: USER) @authenticated { + f(id: String): String @join__field(graph: USER) + } + "#; + + #[test] + fn named_fragment_nested_in_named_fragment_in_authenticated_type() { + static QUERY: &str = r#" + query A($v: Int) { + ... F3 + } + + query B($id:String, $u:Int, $include:Boolean, $skip:Boolean) { + ... F1 + u(u:$u) @include(if: $include) + } + + fragment F1 on Query { + ... F2 + } + + fragment F2 on Query { + t { + ... F3 @skip(if: $skip) + } + } + + fragment F3 on T { + f(id: $id) + } + + fragment F4 on Query { + ...F5 + } + + fragment F5 on Query { + v(v: $v) + } + "#; + + let (doc, paths) = filter(AUTHENTICATED_TYPE_SCHEMA, QUERY); + + insta::assert_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); + } + + #[tokio::test] + async fn introspection_fragment_with_authenticated_root_query() { + static QUERY: &str = r#" + query { + __schema { + types { + ... TypeDef + } + } + } + + fragment TypeDef on __Type { + name + } + "#; + + let service = TestHarness::builder() + .configuration_json(serde_json::json!({ + "supergraph": { + "introspection": true + }, + "include_subgraph_errors": { + "all": true + }, + "authorization": { + "directives": { + "enabled": true + } + }})) + .unwrap() + .schema(AUTHENTICATED_ROOT_TYPE_SCHEMA) + .build_supergraph() + .await + .unwrap(); + + let request = supergraph::Request::fake_builder() + .query(QUERY) + .build() + .unwrap(); + + let mut response = service.oneshot(request).await.unwrap(); + + let first_response = response.next_response().await.unwrap(); + + insta::assert_json_snapshot!(first_response); + + assert!(response.next_response().await.is_none()); + } + + #[tokio::test] + async fn introspection_mixed_with_authenticated_fields() { + // Note: in https://github.com/apollographql/router/pull/5952/ we moved introspection handling + // before authorization filtering in bridge_query_planner.rs, relying on the fact that queries + // mixing introspection and concrete fields are not supported, so introspection answers right + // away. If this ever changes, we should make sure that unauthorized fields are still properly + // filtered out + static QUERY: &str = r#" + query { + __schema { + types { + ... TypeDef + } + } + + t { + f + } + } + + fragment TypeDef on __Type { + name + } + "#; + + let service = TestHarness::builder() + .configuration_json(serde_json::json!({ + "supergraph": { + "introspection": true + }, + "include_subgraph_errors": { + "all": true + }, + "authorization": { + "directives": { + "enabled": true + } + }})) + .unwrap() + .schema(AUTHENTICATED_TYPE_SCHEMA) + .build_supergraph() + .await + .unwrap(); + + let request = supergraph::Request::fake_builder() + .query(QUERY) + .build() + .unwrap(); + + let mut response = service.oneshot(request).await.unwrap(); + + let first_response = response.next_response().await.unwrap(); + + insta::assert_json_snapshot!(first_response); + + assert!(response.next_response().await.is_none()); + } } diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index 8ded941c2b..331641a726 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -437,7 +437,6 @@ impl AuthorizationPlugin { ) -> Result)>, QueryPlannerError> { if let Some(mut visitor) = AuthenticatedVisitor::new( schema.supergraph_schema(), - doc, &schema.implementers_map, dry_run, ) { @@ -475,7 +474,6 @@ impl AuthorizationPlugin { ) -> Result)>, QueryPlannerError> { if let Some(mut visitor) = ScopeFilteringVisitor::new( schema.supergraph_schema(), - doc, &schema.implementers_map, scopes.iter().cloned().collect(), dry_run, @@ -510,7 +508,6 @@ impl AuthorizationPlugin { ) -> Result)>, QueryPlannerError> { if let Some(mut visitor) = PolicyFilteringVisitor::new( schema.supergraph_schema(), - doc, &schema.implementers_map, policies.iter().cloned().collect(), dry_run, diff --git a/apollo-router/src/plugins/authorization/policy.rs b/apollo-router/src/plugins/authorization/policy.rs index df692cf388..821546a01c 100644 --- a/apollo-router/src/plugins/authorization/policy.rs +++ b/apollo-router/src/plugins/authorization/policy.rs @@ -20,6 +20,7 @@ use tower::BoxError; use crate::json_ext::Path; use crate::json_ext::PathElement; use crate::spec::query::transform; +use crate::spec::query::transform::TransformState; use crate::spec::query::traverse; use crate::spec::Schema; use crate::spec::TYPENAME; @@ -187,7 +188,7 @@ impl<'a> traverse::Visitor for PolicyExtractionVisitor<'a> { pub(crate) struct PolicyFilteringVisitor<'a> { schema: &'a schema::Schema, - fragments: HashMap<&'a Name, &'a ast::FragmentDefinition>, + state: TransformState, implementers_map: &'a apollo_compiler::collections::HashMap, dry_run: bool, request_policies: HashSet, @@ -195,7 +196,7 @@ pub(crate) struct PolicyFilteringVisitor<'a> { pub(crate) unauthorized_paths: Vec, // store the error paths from fragments so we can add them at // the point of application - fragments_unauthorized_paths: HashMap<&'a Name, Vec>, + fragments_unauthorized_paths: HashMap>, current_path: Path, policy_directive_name: String, } @@ -222,14 +223,13 @@ fn policies_sets_argument( impl<'a> PolicyFilteringVisitor<'a> { pub(crate) fn new( schema: &'a schema::Schema, - executable: &'a ast::Document, implementers_map: &'a apollo_compiler::collections::HashMap, successful_policies: HashSet, dry_run: bool, ) -> Option { Some(Self { schema, - fragments: transform::collect_fragments(executable), + state: TransformState::new(), implementers_map, dry_run, request_policies: successful_policies, @@ -525,17 +525,11 @@ impl<'a> transform::Visitor for PolicyFilteringVisitor<'a> { }; if self.unauthorized_paths.len() > current_unauthorized_paths_index { - if let Some((name, _)) = self.fragments.get_key_value(&node.name) { - self.fragments_unauthorized_paths.insert( - name, - self.unauthorized_paths - .split_off(current_unauthorized_paths_index), - ); - } - } - - if let Ok(None) = res { - self.fragments.remove(&node.name); + self.fragments_unauthorized_paths.insert( + node.name.as_str().to_string(), + self.unauthorized_paths + .split_off(current_unauthorized_paths_index), + ); } res @@ -546,29 +540,35 @@ impl<'a> transform::Visitor for PolicyFilteringVisitor<'a> { node: &ast::FragmentSpread, ) -> Result, BoxError> { // record the fragment errors at the point of application - if let Some(paths) = self.fragments_unauthorized_paths.get(&node.fragment_name) { + if let Some(paths) = self + .fragments_unauthorized_paths + .get(node.fragment_name.as_str()) + { for path in paths { let path = self.current_path.join(path); self.unauthorized_paths.push(path); } } - let fragment = match self.fragments.get(&node.fragment_name) { - Some(fragment) => fragment, + let condition = match self + .state() + .fragments() + .get(node.fragment_name.as_str()) + .map(|fragment| fragment.fragment.type_condition.clone()) + { + Some(condition) => condition, None => return Ok(None), }; - let condition = &fragment.type_condition; - - self.current_path - .push(PathElement::Fragment(condition.as_str().into())); - let fragment_is_authorized = self .schema .types - .get(condition) + .get(condition.as_str()) .is_some_and(|ty| self.is_type_authorized(ty)); + self.current_path + .push(PathElement::Fragment(condition.as_str().into())); + let res = if !fragment_is_authorized { self.query_requires_policies = true; self.unauthorized_paths.push(self.current_path.clone()); @@ -631,6 +631,10 @@ impl<'a> transform::Visitor for PolicyFilteringVisitor<'a> { fn schema(&self) -> &apollo_compiler::Schema { self.schema } + + fn state(&mut self) -> &mut TransformState { + &mut self.state + } } #[cfg(test)] @@ -744,8 +748,7 @@ mod tests { let doc = ast::Document::parse(query, "query.graphql").unwrap(); doc.to_executable_validate(&schema).unwrap(); let map = schema.implementers_map(); - let mut visitor = - PolicyFilteringVisitor::new(&schema, &doc, &map, policies, false).unwrap(); + let mut visitor = PolicyFilteringVisitor::new(&schema, &map, policies, false).unwrap(); ( transform::document(&mut visitor, &doc).unwrap(), visitor.unauthorized_paths, diff --git a/apollo-router/src/plugins/authorization/scopes.rs b/apollo-router/src/plugins/authorization/scopes.rs index 361b50daad..55a4d0a0c4 100644 --- a/apollo-router/src/plugins/authorization/scopes.rs +++ b/apollo-router/src/plugins/authorization/scopes.rs @@ -20,6 +20,7 @@ use tower::BoxError; use crate::json_ext::Path; use crate::json_ext::PathElement; use crate::spec::query::transform; +use crate::spec::query::transform::TransformState; use crate::spec::query::traverse; use crate::spec::Schema; use crate::spec::TYPENAME; @@ -204,14 +205,14 @@ fn scopes_sets_argument(directive: &ast::Directive) -> impl Iterator { schema: &'a schema::Schema, - fragments: HashMap<&'a Name, &'a ast::FragmentDefinition>, + state: TransformState, implementers_map: &'a apollo_compiler::collections::HashMap, request_scopes: HashSet, pub(crate) query_requires_scopes: bool, pub(crate) unauthorized_paths: Vec, // store the error paths from fragments so we can add them at // the point of application - fragments_unauthorized_paths: HashMap<&'a Name, Vec>, + fragments_unauthorized_paths: HashMap>, current_path: Path, requires_scopes_directive_name: String, dry_run: bool, @@ -220,14 +221,13 @@ pub(crate) struct ScopeFilteringVisitor<'a> { impl<'a> ScopeFilteringVisitor<'a> { pub(crate) fn new( schema: &'a schema::Schema, - executable: &'a ast::Document, implementers_map: &'a apollo_compiler::collections::HashMap, scopes: HashSet, dry_run: bool, ) -> Option { Some(Self { schema, - fragments: transform::collect_fragments(executable), + state: TransformState::new(), implementers_map, request_scopes: scopes, dry_run, @@ -527,17 +527,11 @@ impl<'a> transform::Visitor for ScopeFilteringVisitor<'a> { }; if self.unauthorized_paths.len() > current_unauthorized_paths_index { - if let Some((name, _)) = self.fragments.get_key_value(&node.name) { - self.fragments_unauthorized_paths.insert( - name, - self.unauthorized_paths - .split_off(current_unauthorized_paths_index), - ); - } - } - - if let Ok(None) = res { - self.fragments.remove(&node.name); + self.fragments_unauthorized_paths.insert( + node.name.as_str().to_string(), + self.unauthorized_paths + .split_off(current_unauthorized_paths_index), + ); } res @@ -548,29 +542,35 @@ impl<'a> transform::Visitor for ScopeFilteringVisitor<'a> { node: &ast::FragmentSpread, ) -> Result, BoxError> { // record the fragment errors at the point of application - if let Some(paths) = self.fragments_unauthorized_paths.get(&node.fragment_name) { + if let Some(paths) = self + .fragments_unauthorized_paths + .get(node.fragment_name.as_str()) + { for path in paths { let path = self.current_path.join(path); self.unauthorized_paths.push(path); } } - let fragment = match self.fragments.get(&node.fragment_name) { - Some(fragment) => fragment, + let condition = match self + .state() + .fragments() + .get(node.fragment_name.as_str()) + .map(|fragment| fragment.fragment.type_condition.clone()) + { + Some(condition) => condition, None => return Ok(None), }; - let condition = &fragment.type_condition; - - self.current_path - .push(PathElement::Fragment(condition.as_str().into())); - let fragment_is_authorized = self .schema .types - .get(condition) + .get(condition.as_str()) .is_some_and(|ty| self.is_type_authorized(ty)); + self.current_path + .push(PathElement::Fragment(condition.as_str().into())); + let res = if !fragment_is_authorized { self.query_requires_scopes = true; self.unauthorized_paths.push(self.current_path.clone()); @@ -633,6 +633,10 @@ impl<'a> transform::Visitor for ScopeFilteringVisitor<'a> { fn schema(&self) -> &apollo_compiler::Schema { self.schema } + + fn state(&mut self) -> &mut TransformState { + &mut self.state + } } #[cfg(test)] @@ -748,7 +752,7 @@ mod tests { doc.to_executable_validate(&schema).unwrap(); let map = schema.implementers_map(); - let mut visitor = ScopeFilteringVisitor::new(&schema, &doc, &map, scopes, false).unwrap(); + let mut visitor = ScopeFilteringVisitor::new(&schema, &map, scopes, false).unwrap(); ( transform::document(&mut visitor, &doc).unwrap(), visitor.unauthorized_paths, diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__introspection_fragment_with_authenticated_root_query.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__introspection_fragment_with_authenticated_root_query.snap new file mode 100644 index 0000000000..b7f895cc65 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__introspection_fragment_with_authenticated_root_query.snap @@ -0,0 +1,48 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: first_response +--- +{ + "data": { + "__schema": { + "types": [ + { + "name": "Query" + }, + { + "name": "T" + }, + { + "name": "String" + }, + { + "name": "Boolean" + }, + { + "name": "__Schema" + }, + { + "name": "__Type" + }, + { + "name": "__TypeKind" + }, + { + "name": "__Field" + }, + { + "name": "__InputValue" + }, + { + "name": "__EnumValue" + }, + { + "name": "__Directive" + }, + { + "name": "__DirectiveLocation" + } + ] + } + } +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__introspection_mixed_with_authenticated_fields.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__introspection_mixed_with_authenticated_fields.snap new file mode 100644 index 0000000000..90938cf936 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__introspection_mixed_with_authenticated_fields.snap @@ -0,0 +1,14 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: first_response +--- +{ + "errors": [ + { + "message": "Mixed queries with both schema introspection and concrete fields are not supported", + "extensions": { + "code": "MIXED_INTROSPECTION" + } + } + ] +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__named_fragment_nested_in_authenticated_type.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__named_fragment_nested_in_authenticated_type.snap new file mode 100644 index 0000000000..8a6196b0d8 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__named_fragment_nested_in_authenticated_type.snap @@ -0,0 +1,19 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: "TestResult { query: QUERY, result: doc, paths }" +--- +query: + + query { + t { + ... F + } + } + + fragment F on T { + f + } + +filtered: + +paths: [""] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__named_fragment_nested_in_named_fragment_in_authenticated_type.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__named_fragment_nested_in_named_fragment_in_authenticated_type.snap new file mode 100644 index 0000000000..f2370303d1 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__named_fragment_nested_in_named_fragment_in_authenticated_type.snap @@ -0,0 +1,43 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: "TestResult { query: QUERY, result: doc, paths }" +--- +query: + + query A($v: Int) { + ... F3 + } + + query B($id:String, $u:Int, $include:Boolean, $skip:Boolean) { + ... F1 + u(u:$u) @include(if: $include) + } + + fragment F1 on Query { + ... F2 + } + + fragment F2 on Query { + t { + ... F3 @skip(if: $skip) + } + } + + fragment F3 on T { + f(id: $id) + } + + fragment F4 on Query { + ...F5 + } + + fragment F5 on Query { + v(v: $v) + } + +filtered: +query B($u: Int, $include: Boolean) { + u(u: $u) @include(if: $include) +} + +paths: ["", "/t"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-2.snap index 0ae4015930..fd2f876120 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-2.snap @@ -21,11 +21,7 @@ query: extracted_policies: {"read user", "read username"} successful policies: ["read user", "read username"] filtered: -fragment F on User { - name -} - -query { +{ topProducts { type } @@ -35,4 +31,8 @@ query { } } +fragment F on User { + name +} + paths: [] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap index 1028c92ffc..709b16a40b 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap @@ -22,11 +22,7 @@ query: extracted_scopes: {"read:user", "read:username"} request scopes: ["read:user"] filtered: -fragment F on User { - id2: id -} - -query { +{ topProducts { type } @@ -36,4 +32,8 @@ query { } } +fragment F on User { + id2: id +} + paths: ["/itf/name"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap index 9496930257..410f0562cb 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap @@ -22,12 +22,7 @@ query: extracted_scopes: {"read:user", "read:username"} request scopes: ["read:user", "read:username"] filtered: -fragment F on User { - id2: id - name -} - -query { +{ topProducts { type } @@ -37,4 +32,9 @@ query { } } +fragment F on User { + id2: id + name +} + paths: [] diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index b843f087f2..040ec45a33 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -958,6 +958,97 @@ impl BridgeQueryPlanner { mut key: QueryKey, mut doc: ParsedDocument, ) -> Result { + let mut query_metrics = Default::default(); + let mut selections = self + .parse_selections( + key.original_query.clone(), + key.operation_name.as_deref(), + &doc, + &mut query_metrics, + ) + .await?; + + if selections + .operation(key.operation_name.as_deref()) + .is_some_and(|op| op.selection_set.is_empty()) + { + // All selections have @skip(true) or @include(false) + // Return an empty response now to avoid dealing with an empty query plan later + return Ok(QueryPlannerContent::Response { + response: Box::new( + graphql::Response::builder() + .data(Value::Object(Default::default())) + .build(), + ), + }); + } + + { + let operation = doc + .executable + .operations + .get(key.operation_name.as_deref()) + .ok(); + let mut has_root_typename = false; + let mut has_schema_introspection = false; + let mut has_other_root_fields = false; + if let Some(operation) = operation { + for field in operation.root_fields(&doc.executable) { + match field.name.as_str() { + "__typename" => has_root_typename = true, + "__schema" | "__type" if operation.is_query() => { + has_schema_introspection = true + } + _ => has_other_root_fields = true, + } + } + if has_root_typename && !has_schema_introspection && !has_other_root_fields { + // Fast path for __typename alone + if operation + .selection_set + .selections + .iter() + .all(|sel| sel.as_field().is_some_and(|f| f.name == "__typename")) + { + let root_type_name: serde_json_bytes::ByteString = + operation.object_type().as_str().into(); + let data = Value::Object( + operation + .root_fields(&doc.executable) + .filter(|field| field.name == "__typename") + .map(|field| { + ( + field.response_key().as_str().into(), + Value::String(root_type_name.clone()), + ) + }) + .collect(), + ); + return Ok(QueryPlannerContent::Response { + response: Box::new(graphql::Response::builder().data(data).build()), + }); + } else { + // fragments might use @include or @skip + } + } + } else { + // Should be unreachable as QueryAnalysisLayer would have returned an error + } + + if has_schema_introspection { + if has_other_root_fields { + let error = graphql::Error::builder() + .message("Mixed queries with both schema introspection and concrete fields are not supported") + .extension_code("MIXED_INTROSPECTION") + .build(); + return Ok(QueryPlannerContent::Response { + response: Box::new(graphql::Response::builder().error(error).build()), + }); + } + return self.introspection(key, doc).await; + } + } + let filter_res = if self.enable_authorization_directives { match AuthorizationPlugin::filter_query(&self.configuration, &key, &self.schema) { Err(QueryPlannerError::Unauthorized(unauthorized_paths)) => { @@ -986,16 +1077,6 @@ impl BridgeQueryPlanner { None }; - let mut query_metrics = Default::default(); - let mut selections = self - .parse_selections( - key.original_query.clone(), - key.operation_name.as_deref(), - &doc, - &mut query_metrics, - ) - .await?; - if let Some((unauthorized_paths, new_doc)) = filter_res { key.filtered_query = new_doc.to_string(); let executable_document = new_doc @@ -1016,85 +1097,6 @@ impl BridgeQueryPlanner { selections.unauthorized.paths = unauthorized_paths; } - if selections - .operation(key.operation_name.as_deref()) - .is_some_and(|op| op.selection_set.is_empty()) - { - // All selections have @skip(true) or @include(false) - // Return an empty response now to avoid dealing with an empty query plan later - return Ok(QueryPlannerContent::Response { - response: Box::new( - graphql::Response::builder() - .data(Value::Object(Default::default())) - .build(), - ), - }); - } - - let operation = doc - .executable - .operations - .get(key.operation_name.as_deref()) - .ok(); - let mut has_root_typename = false; - let mut has_schema_introspection = false; - let mut has_other_root_fields = false; - if let Some(operation) = operation { - for field in operation.root_fields(&doc.executable) { - match field.name.as_str() { - "__typename" => has_root_typename = true, - "__schema" | "__type" if operation.is_query() => { - has_schema_introspection = true - } - _ => has_other_root_fields = true, - } - } - if has_root_typename && !has_schema_introspection && !has_other_root_fields { - // Fast path for __typename alone - if operation - .selection_set - .selections - .iter() - .all(|sel| sel.as_field().is_some_and(|f| f.name == "__typename")) - { - let root_type_name: serde_json_bytes::ByteString = - operation.object_type().as_str().into(); - let data = Value::Object( - operation - .root_fields(&doc.executable) - .filter(|field| field.name == "__typename") - .map(|field| { - ( - field.response_key().as_str().into(), - Value::String(root_type_name.clone()), - ) - }) - .collect(), - ); - return Ok(QueryPlannerContent::Response { - response: Box::new(graphql::Response::builder().data(data).build()), - }); - } else { - // fragments might use @include or @skip - } - } - } else { - // Should be unreachable as QueryAnalysisLayer would have returned an error - } - - if has_schema_introspection { - if has_other_root_fields { - let error = graphql::Error::builder() - .message("Mixed queries with both schema introspection and concrete fields are not supported") - .extension_code("MIXED_INTROSPECTION") - .build(); - return Ok(QueryPlannerContent::Response { - response: Box::new(graphql::Response::builder().error(error).build()), - }); - } - return self.introspection(key, doc).await; - } - if key.filtered_query != key.original_query { let mut filtered = self .parse_selections( diff --git a/apollo-router/src/query_planner/labeler.rs b/apollo-router/src/query_planner/labeler.rs index a993e1dbd3..856dc7385c 100644 --- a/apollo-router/src/query_planner/labeler.rs +++ b/apollo-router/src/query_planner/labeler.rs @@ -11,6 +11,7 @@ use tower::BoxError; use crate::spec::query::subselections::DEFER_DIRECTIVE_NAME; use crate::spec::query::transform; use crate::spec::query::transform::document; +use crate::spec::query::transform::TransformState; use crate::spec::query::transform::Visitor; const LABEL_NAME: Name = name!("label"); @@ -25,12 +26,14 @@ pub(crate) fn add_defer_labels( let mut visitor = Labeler { next_label: 0, schema, + state: TransformState::new(), }; document(&mut visitor, doc) } pub(crate) struct Labeler<'a> { schema: &'a Schema, + state: TransformState, next_label: u32, } @@ -65,6 +68,10 @@ impl Visitor for Labeler<'_> { fn schema(&self) -> &apollo_compiler::Schema { self.schema } + + fn state(&mut self) -> &mut TransformState { + &mut self.state + } } fn directives( diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-2.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-2.snap new file mode 100644 index 0000000000..4f85ed0409 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-2.snap @@ -0,0 +1,14 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String) { + a(arg: $a) @remove + c + } +filtered: +{ + c +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-3.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-3.snap new file mode 100644 index 0000000000..de9dd21421 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-3.snap @@ -0,0 +1,18 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String) { + ... F + c + } + + fragment F on Query { + a(arg: $a) @remove + } +filtered: +{ + c +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-4.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-4.snap new file mode 100644 index 0000000000..160529a1de --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-4.snap @@ -0,0 +1,18 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String) { + ... F @remove + c + } + + fragment F on Query { + a(arg: $a) + } +filtered: +{ + c +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-5.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-5.snap new file mode 100644 index 0000000000..5f40386191 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-5.snap @@ -0,0 +1,23 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String) { + ... F @remove + c + } + + fragment F on Query { + ... G + } + + fragment G on Query { + a(arg: $a) + } + +filtered: +{ + c +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-6.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-6.snap new file mode 100644 index 0000000000..d2f11f22a9 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-6.snap @@ -0,0 +1,23 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String) { + ... F + c + } + + fragment F on Query { + ... G + } + + fragment G on Query { + a(arg: $a) @remove + } + +filtered: +{ + c +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive.snap new file mode 100644 index 0000000000..3740848613 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive.snap @@ -0,0 +1,20 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query { + a + ... F @remove + } + + fragment F on Query { + b { + a + } + } +filtered: +{ + a +} diff --git a/apollo-router/src/spec/query/transform.rs b/apollo-router/src/spec/query/transform.rs index b307d9a2d8..078634f1d6 100644 --- a/apollo-router/src/spec/query/transform.rs +++ b/apollo-router/src/spec/query/transform.rs @@ -1,8 +1,9 @@ +use std::collections::BTreeMap; use std::collections::HashMap; +use std::collections::HashSet; use apollo_compiler::ast; use apollo_compiler::schema::FieldLookupError; -use apollo_compiler::Name; use tower::BoxError; /// Transform a document with the given visitor. @@ -15,17 +16,46 @@ pub(crate) fn document( definitions: Vec::new(), }; - // walk through the fragment first: if a fragment is entirely filtered, we want to + // go through the fragments and order them, starting with the ones that reference no other fragments + // then the ones that depend only on the first one, and so on + // This allows visitors like authorization to have all the required information if they encounter + // a fragment spread while filtering a fragment + let mut fragment_visitor = FragmentOrderVisitor::new(); + fragment_visitor.visit_document(document); + let ordered_fragments = fragment_visitor.ordered_fragments(); + + visitor.state().reset(); + + // Then walk again through the fragments: if a fragment is entirely filtered, we want to // remove the spread too - for definition in &document.definitions { - if let ast::Definition::FragmentDefinition(def) = definition { - if let Some(new_def) = visitor.fragment_definition(def)? { - new.definitions - .push(ast::Definition::FragmentDefinition(new_def.into())) - } + for def in ordered_fragments { + visitor.state().used_fragments.clear(); + visitor.state().used_variables.clear(); + + if let Some(new_def) = visitor.fragment_definition(def)? { + // keep the list of used variables per fragment, as we need to use it to know which variables are used + // in a query + let used_variables = visitor.state().used_variables.clone(); + + // keep the list of used fragments per fragment, as we need to use it to gather used variables later + // unfortunately, we may not know the variable used for those fragments at this point, as they may not + // have been processed yet + let local_used_fragments = visitor.state().used_fragments.clone(); + + visitor.state().defined_fragments.insert( + def.name.as_str().to_string(), + DefinedFragment { + fragment: new_def, + used_variables, + used_fragments: local_used_fragments, + }, + ); } } + // keeps the list of fragments used in the produced document (some fragment spreads might have been removed) + let mut used_fragments = HashSet::new(); + for definition in &document.definitions { if let ast::Definition::OperationDefinition(def) = definition { let root_type = visitor @@ -33,18 +63,116 @@ pub(crate) fn document( .root_operation(def.operation_type) .ok_or("missing root operation definition")? .clone(); - if let Some(new_def) = visitor.operation(&root_type, def)? { + + // we reset the used_fragments and used_variables lists for each operation + visitor.state().used_fragments.clear(); + visitor.state().used_variables.clear(); + if let Some(mut new_def) = visitor.operation(&root_type, def)? { + let mut local_used_fragments = visitor.state().used_fragments.clone(); + + // gather the entire list of fragments used in this operation + loop { + let mut new_local_used_fragments = local_used_fragments.clone(); + for fragment_name in local_used_fragments.iter() { + if let Some(defined_fragment) = visitor + .state() + .defined_fragments + .get(fragment_name.as_str()) + { + new_local_used_fragments + .extend(defined_fragment.used_fragments.clone()); + } + } + + // no more changes, we can stop + if new_local_used_fragments.len() == local_used_fragments.len() { + break; + } + local_used_fragments = new_local_used_fragments; + } + + // add to the list of used variables all the variables used in the fragment spreads + for fragment_name in local_used_fragments.iter() { + if let Some(defined_fragment_used_variables) = visitor + .state() + .defined_fragments + .get(fragment_name.as_str()) + .map(|defined_fragment| defined_fragment.used_variables.clone()) + { + visitor + .state() + .used_variables + .extend(defined_fragment_used_variables); + } + } + used_fragments.extend(local_used_fragments); + + // remove unused variables + new_def.variables.retain(|var| { + let res = visitor.state().used_variables.contains(var.name.as_str()); + res + }); + new.definitions - .push(ast::Definition::OperationDefinition(new_def.into())) + .push(ast::Definition::OperationDefinition(new_def.into())); } } } + + for (name, defined_fragment) in visitor.state().defined_fragments.clone().into_iter() { + if used_fragments.contains(name.as_str()) { + new.definitions.push(ast::Definition::FragmentDefinition( + defined_fragment.fragment.into(), + )); + } + } Ok(new) } +/// Holds state during the transformation to account for used fragments and variables. +pub(crate) struct TransformState { + used_fragments: HashSet, + used_variables: HashSet, + /// keeps the list of fragments defined in the produced document (the visitor might have removed some of them) + defined_fragments: BTreeMap, +} + +#[derive(Clone)] +pub(crate) struct DefinedFragment { + pub(crate) fragment: ast::FragmentDefinition, + /// variables used in the fragment + pub(crate) used_variables: HashSet, + /// fragments used in the fragment + pub(crate) used_fragments: HashSet, +} + +impl TransformState { + pub(crate) fn new() -> Self { + Self { + used_fragments: HashSet::new(), + used_variables: HashSet::new(), + defined_fragments: BTreeMap::new(), + } + } + + fn reset(&mut self) { + self.used_fragments.clear(); + self.used_variables.clear(); + self.defined_fragments.clear(); + } + + pub(crate) fn fragments(&self) -> &BTreeMap { + &self.defined_fragments + } +} + pub(crate) trait Visitor: Sized { fn schema(&self) -> &apollo_compiler::Schema; + /// mutable state provided by the visitor to clean up unused fragments and variables + /// do not modify directly + fn state(&mut self) -> &mut TransformState; + /// Transform an operation definition. /// /// Call the [`operation`] free function for the default behavior. @@ -89,7 +217,13 @@ pub(crate) trait Visitor: Sized { &mut self, def: &ast::FragmentSpread, ) -> Result, BoxError> { - fragment_spread(self, def) + let res = fragment_spread(self, def); + if let Ok(Some(ref fragment)) = res.as_ref() { + self.state() + .used_fragments + .insert(fragment.fragment_name.as_str().to_string()); + } + res } /// Transform a inline fragment within a selection set. @@ -158,6 +292,27 @@ pub(crate) fn field( else { return Ok(None); }; + + for argument in def.arguments.iter() { + if let Some(var) = argument.value.as_variable() { + visitor + .state() + .used_variables + .insert(var.as_str().to_string()); + } + } + + for directive in def.directives.iter() { + for argument in directive.arguments.iter() { + if let Some(var) = argument.value.as_variable() { + visitor + .state() + .used_variables + .insert(var.as_str().to_string()); + } + } + } + Ok(Some(ast::Field { alias: def.alias.clone(), name: def.name.clone(), @@ -174,7 +329,22 @@ pub(crate) fn fragment_spread( visitor: &mut impl Visitor, def: &ast::FragmentSpread, ) -> Result, BoxError> { - let _ = visitor; // Unused, but matches trait method signature + visitor + .state() + .used_fragments + .insert(def.fragment_name.as_str().to_string()); + + for directive in def.directives.iter() { + for argument in directive.arguments.iter() { + if let Some(var) = argument.value.as_variable() { + visitor + .state() + .used_variables + .insert(var.as_str().to_string()); + } + } + } + Ok(Some(def.clone())) } @@ -189,6 +359,18 @@ pub(crate) fn inline_fragment( let Some(selection_set) = selection_set(visitor, parent_type, &def.selection_set)? else { return Ok(None); }; + + for directive in def.directives.iter() { + for argument in directive.arguments.iter() { + if let Some(var) = argument.value.as_variable() { + visitor + .state() + .used_variables + .insert(var.as_str().to_string()); + } + } + } + Ok(Some(ast::InlineFragment { type_condition: def.type_condition.clone(), directives: def.directives.clone(), @@ -242,50 +424,154 @@ pub(crate) fn selection_set( Ok((!selections.is_empty()).then_some(selections)) } -pub(crate) fn collect_fragments( - executable: &ast::Document, -) -> HashMap<&Name, &ast::FragmentDefinition> { - executable - .definitions - .iter() - .filter_map(|def| match def { - ast::Definition::FragmentDefinition(frag) => Some((&frag.name, frag.as_ref())), - _ => None, - }) - .collect() +/// this visitor goes through the list of fragments in the query, looking at fragment spreads +/// in their selection, and generates a list of fragments in the order they should be visited +/// by the transform visitor, to ensure a fragment has already been visited before it is +/// referenced in a fragment spread +struct FragmentOrderVisitor<'a> { + // the resulting list of ordered fragments + ordered_fragments: Vec, + // list of fragments in the document + fragments: HashMap, + + // fragment dependencies. The key is a fragment name, the value is all the fragments that reference it + // in a fragment spread + dependencies: HashMap>, + // name of the fragment currently being visited + current: Option, + + // how many fragments are used by each fragment. This is decremented when a referenced fragment + // is added to the final list. Once it reaches 0, the fragment is added to the final list too + rank: HashMap, } -#[test] -fn test_add_directive_to_fields() { - struct AddDirective { - schema: apollo_compiler::Schema, +impl<'a> FragmentOrderVisitor<'a> { + fn new() -> Self { + Self { + ordered_fragments: Vec::new(), + fragments: HashMap::new(), + dependencies: HashMap::new(), + current: None, + rank: HashMap::new(), + } } - impl Visitor for AddDirective { - fn field( - &mut self, - _parent_type: &str, - field_def: &ast::FieldDefinition, - def: &ast::Field, - ) -> Result, BoxError> { - Ok(field(self, field_def, def)?.map(|mut new| { - new.directives.push( - ast::Directive { - name: apollo_compiler::name!("added"), - arguments: Vec::new(), + fn rerank(&mut self, name: &str) { + if let Some(v) = self.dependencies.remove(name) { + for dep in v { + if let Some(rank) = self.rank.get_mut(&dep) { + *rank -= 1; + if *rank == 0 { + self.ordered_fragments.push(dep.clone()); + self.rerank(&dep); } - .into(), - ); - new - })) + } + } } + } - fn schema(&self) -> &apollo_compiler::Schema { - &self.schema + fn ordered_fragments(self) -> Vec<&'a ast::FragmentDefinition> { + let mut ordered_fragments = Vec::new(); + for name in self.ordered_fragments { + if let Some(fragment) = self.fragments.get(name.as_str()) { + ordered_fragments.push(*fragment); + } + } + ordered_fragments + } + + fn visit_document(&mut self, doc: &'a ast::Document) { + for definition in &doc.definitions { + if let ast::Definition::FragmentDefinition(def) = definition { + self.visit_fragment_definition(def); + } + } + } + + fn visit_fragment_definition(&mut self, def: &'a ast::FragmentDefinition) { + let name = def.name.as_str().to_string(); + self.fragments.insert(name.clone(), def); + + self.current = Some(name.clone()); + self.rank.insert(name.clone(), 0); + + self.visit_selection_set(&def.selection_set); + + if self.rank.get(&name) == Some(&0) { + // if the fragment does not reference any other fragments, it is ready to be added to the final list + self.ordered_fragments.push(name.clone()); + // then we rerank all the fragments that reference this one: if any of them reaches the rank 0, they + // are added to the final list too + self.rerank(&name); } } - let graphql = " + fn visit_selection_set(&mut self, selection_set: &[apollo_compiler::ast::Selection]) { + for selection in selection_set { + match selection { + ast::Selection::Field(def) => self.visit_selection_set(&def.selection_set), + ast::Selection::InlineFragment(def) => self.visit_selection_set(&def.selection_set), + ast::Selection::FragmentSpread(def) => { + let name = def.fragment_name.as_str().to_string(); + + // we have already seen this fragment, so we don't need to add it again + if self.rank.get(name.as_str()) == Some(&0) { + continue; + } + if let Some(current) = self.current.as_ref() { + if let Some(rank) = self.rank.get_mut(current.as_str()) { + *rank += 1; + } + self.dependencies + .entry(name) + .or_default() + .push(current.clone()); + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_add_directive_to_fields() { + struct AddDirective { + schema: apollo_compiler::Schema, + state: TransformState, + } + + impl Visitor for AddDirective { + fn field( + &mut self, + _parent_type: &str, + field_def: &ast::FieldDefinition, + def: &ast::Field, + ) -> Result, BoxError> { + Ok(field(self, field_def, def)?.map(|mut new| { + new.directives.push( + ast::Directive { + name: apollo_compiler::name!("added"), + arguments: Vec::new(), + } + .into(), + ); + new + })) + } + + fn schema(&self) -> &apollo_compiler::Schema { + &self.schema + } + + fn state(&mut self) -> &mut TransformState { + &mut self.state + } + } + + let graphql = " type Query { a(id: ID): String b: Int @@ -307,23 +593,239 @@ fn test_add_directive_to_fields() { } } "; - let ast = apollo_compiler::ast::Document::parse(graphql, "").unwrap(); - let (schema, _doc) = ast.to_mixed_validate().unwrap(); - let schema = schema.into_inner(); - let mut visitor = AddDirective { schema }; - let expected = "fragment F on Query { - next @added { - a @added - } -} - -query($id: ID = null) { + let ast = apollo_compiler::ast::Document::parse(graphql, "").unwrap(); + let (schema, _doc) = ast.to_mixed_validate().unwrap(); + let schema = schema.into_inner(); + let mut visitor = AddDirective { + schema, + state: TransformState::new(), + }; + let expected = "query($id: ID = null) { a(id: $id) @added ... @defer { b @added } ...F } + +fragment F on Query { + next @added { + a @added + } +} "; - assert_eq!(document(&mut visitor, &ast).unwrap().to_string(), expected) + assert_eq!(document(&mut visitor, &ast).unwrap().to_string(), expected) + } + + struct RemoveDirective { + schema: apollo_compiler::Schema, + state: TransformState, + } + + impl RemoveDirective { + fn new(schema: apollo_compiler::Schema) -> Self { + Self { + schema, + state: TransformState::new(), + } + } + } + + impl Visitor for RemoveDirective { + fn field( + &mut self, + _parent_type: &str, + field_def: &ast::FieldDefinition, + def: &ast::Field, + ) -> Result, BoxError> { + if def.directives.iter().any(|d| d.name == "remove") { + return Ok(None); + } + field(self, field_def, def) + } + + fn fragment_spread( + &mut self, + def: &ast::FragmentSpread, + ) -> Result, BoxError> { + if def.directives.iter().any(|d| d.name == "remove") { + return Ok(None); + } + + // remove the fragment spread if the fragment was removed + if !self + .state() + .fragments() + .contains_key(def.fragment_name.as_str()) + { + return Ok(None); + } + + fragment_spread(self, def) + } + + fn inline_fragment( + &mut self, + _parent_type: &str, + def: &ast::InlineFragment, + ) -> Result, BoxError> { + if def.directives.iter().any(|d| d.name == "remove") { + return Ok(None); + } + inline_fragment(self, _parent_type, def) + } + + fn schema(&self) -> &apollo_compiler::Schema { + &self.schema + } + + fn state(&mut self) -> &mut TransformState { + &mut self.state + } + } + + struct TestResult<'a> { + query: &'a str, + result: ast::Document, + } + + impl<'a> std::fmt::Display for TestResult<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "query:\n{}\nfiltered:\n{}", self.query, self.result,) + } + } + + static TRANSFORM_REMOVE_SCHEMA: &str = r#" + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/authenticated/v0.1", for: SECURITY) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @remove on FIELD | INLINE_FRAGMENT | FRAGMENT_SPREAD + 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 { + a(arg: String): String + b: Obj + c: Int + } + + type Obj { + a: String + } + "#; + + #[test] + fn remove_directive() { + let ast = apollo_compiler::ast::Document::parse(TRANSFORM_REMOVE_SCHEMA, "").unwrap(); + let (schema, _doc) = ast.to_mixed_validate().unwrap(); + let schema = schema.into_inner(); + let mut visitor = RemoveDirective::new(schema.clone()); + + // test removed fragment + let query = r#" + query { + a + ... F @remove + } + + fragment F on Query { + b { + a + } + }"#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable + let query = r#" + query($a: String) { + a(arg: $a) @remove + c + }"#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in fragment + let query = r#" + query($a: String) { + ... F + c + } + + fragment F on Query { + a(arg: $a) @remove + }"#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test field with variable in removed fragment + let query = r#" + query($a: String) { + ... F @remove + c + } + + fragment F on Query { + a(arg: $a) + }"#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test field with variable in fragment nested in removed fragment + let query = r#" + query($a: String) { + ... F @remove + c + } + + fragment F on Query { + ... G + } + + fragment G on Query { + a(arg: $a) + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in fragment nested in fragment + let query = r#" + query($a: String) { + ... F + c + } + + fragment F on Query { + ... G + } + + fragment G on Query { + a(arg: $a) @remove + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + } } From 7e7e05fd3cddbb63ab2e61fdb9c4d33f65e0afa4 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 11 Sep 2024 08:11:52 -0700 Subject: [PATCH 16/56] Allow disabling PQ-based QP cache rewarm when reloading schemas (#5990) --- .changesets/exp_qp_cache_prewarm_on_reload.md | 20 ++++++++++++ ...ml => 0028-entity_cache_type_metrics.yaml} | 0 .../0029-pq-prewarm-to-on_startup.yaml | 12 +++++++ apollo-router/src/configuration/mod.rs | 1 + .../src/configuration/persisted_queries.rs | 32 ++++++++++++++----- ...nfiguration__tests__schema_generation.snap | 22 +++++++++++-- ...d-queries-prewarm-already-object.yaml.snap | 9 ++++++ ...@persisted-queries-prewarm-false.yaml.snap | 9 ++++++ ...n@persisted-queries-prewarm-true.yaml.snap | 9 ++++++ ...sisted-queries-prewarm-already-object.yaml | 4 +++ .../persisted-queries-prewarm-false.yaml | 3 ++ .../persisted-queries-prewarm-true.yaml | 3 ++ apollo-router/src/configuration/upgrade.rs | 2 +- .../query_planner/caching_query_planner.rs | 8 +++-- apollo-router/src/router_factory.rs | 4 +-- .../persisted_queries/manifest_poller.rs | 18 +++++------ .../src/services/supergraph/service.rs | 3 +- .../configuration/in-memory-caching.mdx | 6 ++-- .../configuration/persisted-queries.mdx | 6 ++-- 19 files changed, 138 insertions(+), 33 deletions(-) create mode 100644 .changesets/exp_qp_cache_prewarm_on_reload.md rename apollo-router/src/configuration/migrations/{0027.entity_cache_type_metrics.yaml => 0028-entity_cache_type_metrics.yaml} (100%) create mode 100644 apollo-router/src/configuration/migrations/0029-pq-prewarm-to-on_startup.yaml create mode 100644 apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-already-object.yaml.snap create mode 100644 apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-false.yaml.snap create mode 100644 apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-true.yaml.snap create mode 100644 apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-already-object.yaml create mode 100644 apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-false.yaml create mode 100644 apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-true.yaml diff --git a/.changesets/exp_qp_cache_prewarm_on_reload.md b/.changesets/exp_qp_cache_prewarm_on_reload.md new file mode 100644 index 0000000000..56f87a70e7 --- /dev/null +++ b/.changesets/exp_qp_cache_prewarm_on_reload.md @@ -0,0 +1,20 @@ +### Allow disabling persisted-queries-based query plan cache prewarm on schema reload + +In Router v1.31.0, we started including operations from persisted query lists when Router pre-warms the query plan cache when loading a new schema. + +In Router v1.49.0, we let you also pre-warm the query plan cache from the persisted query list during Router startup by setting `persisted_queries.experimental_prewarm_query_plan_cache` to true. + +We now allow you to disable the original feature, so that Router will only pre-warm recent operations from the query planning cache when loading a new schema (and not the persisted query list as well), by setting `persisted_queries.experimental_prewarm_query_plan_cache.on_reload` to `false`. + +The option added in v1.49.0 has been renamed from `persisted_queries.experimental_prewarm_query_plan_cache` to `persisted_queries.experimental_prewarm_query_plan_cache.on_startup`. Existing configuration files will keep working as before, but with a warning that can be resolved by updating your config file: + +```diff + persisted_queries: + enabled: true +- experimental_prewarm_query_plan_cache: true ++ experimental_prewarm_query_plan_cache: ++ on_startup: true +``` + + +By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/5990 \ No newline at end of file diff --git a/apollo-router/src/configuration/migrations/0027.entity_cache_type_metrics.yaml b/apollo-router/src/configuration/migrations/0028-entity_cache_type_metrics.yaml similarity index 100% rename from apollo-router/src/configuration/migrations/0027.entity_cache_type_metrics.yaml rename to apollo-router/src/configuration/migrations/0028-entity_cache_type_metrics.yaml diff --git a/apollo-router/src/configuration/migrations/0029-pq-prewarm-to-on_startup.yaml b/apollo-router/src/configuration/migrations/0029-pq-prewarm-to-on_startup.yaml new file mode 100644 index 0000000000..0c6c755711 --- /dev/null +++ b/apollo-router/src/configuration/migrations/0029-pq-prewarm-to-on_startup.yaml @@ -0,0 +1,12 @@ +description: Experimental persisted query prewarm query plan cache can now be configured separately for `on_startup` and `on_reload`; the previous value now means `on_startup` +actions: + - type: change + path: persisted_queries.experimental_prewarm_query_plan_cache + from: true + to: + on_startup: true + - type: change + path: persisted_queries.experimental_prewarm_query_plan_cache + from: false + to: + on_startup: false diff --git a/apollo-router/src/configuration/mod.rs b/apollo-router/src/configuration/mod.rs index 6fc59167d0..7837d8e4dc 100644 --- a/apollo-router/src/configuration/mod.rs +++ b/apollo-router/src/configuration/mod.rs @@ -15,6 +15,7 @@ use displaydoc::Display; use itertools::Itertools; use once_cell::sync::Lazy; pub(crate) use persisted_queries::PersistedQueries; +pub(crate) use persisted_queries::PersistedQueriesPrewarmQueryPlanCache; #[cfg(test)] pub(crate) use persisted_queries::PersistedQueriesSafelist; use regex::Regex; diff --git a/apollo-router/src/configuration/persisted_queries.rs b/apollo-router/src/configuration/persisted_queries.rs index e1ae76e0e0..2b395b79ff 100644 --- a/apollo-router/src/configuration/persisted_queries.rs +++ b/apollo-router/src/configuration/persisted_queries.rs @@ -16,7 +16,7 @@ pub struct PersistedQueries { pub safelist: PersistedQueriesSafelist, /// Experimental feature to prewarm the query plan cache with persisted queries - pub experimental_prewarm_query_plan_cache: bool, + pub experimental_prewarm_query_plan_cache: PersistedQueriesPrewarmQueryPlanCache, /// Enables using a local copy of the persisted query manifest to safelist operations pub experimental_local_manifests: Option>, @@ -30,7 +30,7 @@ impl PersistedQueries { enabled: Option, log_unknown: Option, safelist: Option, - experimental_prewarm_query_plan_cache: Option, + experimental_prewarm_query_plan_cache: Option, experimental_local_manifests: Option>, ) -> Self { Self { @@ -38,7 +38,7 @@ impl PersistedQueries { safelist: safelist.unwrap_or_default(), log_unknown: log_unknown.unwrap_or_else(default_log_unknown), experimental_prewarm_query_plan_cache: experimental_prewarm_query_plan_cache - .unwrap_or_else(default_prewarm_query_plan_cache), + .unwrap_or_default(), experimental_local_manifests, } } @@ -67,13 +67,24 @@ impl PersistedQueriesSafelist { } } +/// Persisted Queries (PQ) query plan cache prewarm configuration +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields, default)] +pub struct PersistedQueriesPrewarmQueryPlanCache { + /// Enabling this field uses the persisted query list to pre-warm the query planner cache on startup (disabled by default) + pub on_startup: bool, + + /// Enabling this field uses the persisted query list to pre-warm the query planner cache on schema and config changes (enabled by default) + pub on_reload: bool, +} + impl Default for PersistedQueries { fn default() -> Self { Self { enabled: default_pq(), safelist: PersistedQueriesSafelist::default(), log_unknown: default_log_unknown(), - experimental_prewarm_query_plan_cache: default_prewarm_query_plan_cache(), + experimental_prewarm_query_plan_cache: PersistedQueriesPrewarmQueryPlanCache::default(), experimental_local_manifests: None, } } @@ -88,6 +99,15 @@ impl Default for PersistedQueriesSafelist { } } +impl Default for PersistedQueriesPrewarmQueryPlanCache { + fn default() -> Self { + Self { + on_startup: false, + on_reload: true, + } + } +} + const fn default_pq() -> bool { false } @@ -103,7 +123,3 @@ const fn default_require_id() -> bool { const fn default_log_unknown() -> bool { false } - -const fn default_prewarm_query_plan_cache() -> bool { - false -} diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 8789840074..4eba0206d0 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -4119,9 +4119,8 @@ expression: "&schema" "type": "array" }, "experimental_prewarm_query_plan_cache": { - "default": false, - "description": "Experimental feature to prewarm the query plan cache with persisted queries", - "type": "boolean" + "$ref": "#/definitions/PersistedQueriesPrewarmQueryPlanCache", + "description": "#/definitions/PersistedQueriesPrewarmQueryPlanCache" }, "log_unknown": { "default": false, @@ -4135,6 +4134,23 @@ expression: "&schema" }, "type": "object" }, + "PersistedQueriesPrewarmQueryPlanCache": { + "additionalProperties": false, + "description": "Persisted Queries (PQ) query plan cache prewarm configuration", + "properties": { + "on_reload": { + "default": true, + "description": "Enabling this field uses the persisted query list to pre-warm the query planner cache on schema and config changes (enabled by default)", + "type": "boolean" + }, + "on_startup": { + "default": false, + "description": "Enabling this field uses the persisted query list to pre-warm the query planner cache on startup (disabled by default)", + "type": "boolean" + } + }, + "type": "object" + }, "PersistedQueriesSafelist": { "additionalProperties": false, "description": "Persisted Queries (PQ) Safelisting configuration", diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-already-object.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-already-object.yaml.snap new file mode 100644 index 0000000000..8898a55cc9 --- /dev/null +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-already-object.yaml.snap @@ -0,0 +1,9 @@ +--- +source: apollo-router/src/configuration/tests.rs +expression: new_config +--- +--- +persisted_queries: + enabled: true + experimental_prewarm_query_plan_cache: + on_reload: false diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-false.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-false.yaml.snap new file mode 100644 index 0000000000..3d45bd15ac --- /dev/null +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-false.yaml.snap @@ -0,0 +1,9 @@ +--- +source: apollo-router/src/configuration/tests.rs +expression: new_config +--- +--- +persisted_queries: + enabled: true + experimental_prewarm_query_plan_cache: + on_startup: false diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-true.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-true.yaml.snap new file mode 100644 index 0000000000..ef598f91d2 --- /dev/null +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-true.yaml.snap @@ -0,0 +1,9 @@ +--- +source: apollo-router/src/configuration/tests.rs +expression: new_config +--- +--- +persisted_queries: + enabled: true + experimental_prewarm_query_plan_cache: + on_startup: true diff --git a/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-already-object.yaml b/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-already-object.yaml new file mode 100644 index 0000000000..a6647b3a04 --- /dev/null +++ b/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-already-object.yaml @@ -0,0 +1,4 @@ +persisted_queries: + enabled: true + experimental_prewarm_query_plan_cache: + on_reload: false diff --git a/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-false.yaml b/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-false.yaml new file mode 100644 index 0000000000..704865131f --- /dev/null +++ b/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-false.yaml @@ -0,0 +1,3 @@ +persisted_queries: + enabled: true + experimental_prewarm_query_plan_cache: false diff --git a/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-true.yaml b/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-true.yaml new file mode 100644 index 0000000000..3a1fafa87d --- /dev/null +++ b/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-true.yaml @@ -0,0 +1,3 @@ +persisted_queries: + enabled: true + experimental_prewarm_query_plan_cache: true diff --git a/apollo-router/src/configuration/upgrade.rs b/apollo-router/src/configuration/upgrade.rs index 0056801e86..d925ed7354 100644 --- a/apollo-router/src/configuration/upgrade.rs +++ b/apollo-router/src/configuration/upgrade.rs @@ -144,7 +144,7 @@ fn apply_migration(config: &Value, migration: &Migration) -> Result { - if !jsonpath_lib::select(config, &format!("$.{path} == {from}")) + if !jsonpath_lib::select(config, &format!("$[?(@.{path} == {from})]")) .unwrap_or_default() .is_empty() { diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index e2a4053d96..bee5c9a8b5 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -28,6 +28,7 @@ use crate::cache::estimate_size; use crate::cache::storage::InMemoryCache; use crate::cache::storage::ValueType; use crate::cache::DeduplicatingCache; +use crate::configuration::PersistedQueriesPrewarmQueryPlanCache; use crate::error::CacheResolverError; use crate::error::QueryPlannerError; use crate::plugins::authorization::AuthorizationPlugin; @@ -167,7 +168,7 @@ where previous_cache: Option, count: Option, experimental_reuse_query_plans: bool, - experimental_pql_prewarm: bool, + experimental_pql_prewarm: &PersistedQueriesPrewarmQueryPlanCache, ) { let _timer = Timer::new(|duration| { ::tracing::info!( @@ -223,8 +224,9 @@ where cache_keys.shuffle(&mut thread_rng()); - let should_warm_with_pqs = - (experimental_pql_prewarm && previous_cache.is_none()) || previous_cache.is_some(); + let should_warm_with_pqs = (experimental_pql_prewarm.on_startup + && previous_cache.is_none()) + || (experimental_pql_prewarm.on_reload && previous_cache.is_some()); let persisted_queries_operations = persisted_query_layer.all_operations(); let capacity = if should_warm_with_pqs { diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs index bb4c4a1522..fa2bde177f 100644 --- a/apollo-router/src/router_factory.rs +++ b/apollo-router/src/router_factory.rs @@ -253,7 +253,7 @@ impl YamlRouterFactory { .supergraph .query_planning .experimental_reuse_query_plans, - configuration + &configuration .persisted_queries .experimental_prewarm_query_plan_cache, ) @@ -269,7 +269,7 @@ impl YamlRouterFactory { .supergraph .query_planning .experimental_reuse_query_plans, - configuration + &configuration .persisted_queries .experimental_prewarm_query_plan_cache, ) diff --git a/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs b/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs index c957b086e0..7051ec97be 100644 --- a/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs +++ b/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs @@ -686,7 +686,6 @@ mod tests { use super::*; use crate::configuration::Apq; use crate::configuration::PersistedQueries; - use crate::configuration::PersistedQueriesSafelist; use crate::test_harness::mocks::persisted_queries::*; use crate::uplink::Endpoints; @@ -783,15 +782,14 @@ mod tests { let manifest_manager = PersistedQueryManifestPoller::new( Configuration::fake_builder() .apq(Apq::fake_new(Some(false))) - .persisted_query(PersistedQueries::new( - Some(true), - Some(false), - Some(PersistedQueriesSafelist::default()), - Some(false), - Some(vec![ - "tests/fixtures/persisted-queries-manifest.json".to_string() - ]), - )) + .persisted_query( + PersistedQueries::builder() + .enabled(true) + .experimental_local_manifests(vec![ + "tests/fixtures/persisted-queries-manifest.json".to_string(), + ]) + .build(), + ) .build() .unwrap(), ) diff --git a/apollo-router/src/services/supergraph/service.rs b/apollo-router/src/services/supergraph/service.rs index d06edb7092..136f4e9063 100644 --- a/apollo-router/src/services/supergraph/service.rs +++ b/apollo-router/src/services/supergraph/service.rs @@ -28,6 +28,7 @@ use tracing_futures::Instrument; use crate::batching::BatchQuery; use crate::configuration::Batching; +use crate::configuration::PersistedQueriesPrewarmQueryPlanCache; use crate::context::OPERATION_NAME; use crate::error::CacheResolverError; use crate::graphql; @@ -945,7 +946,7 @@ impl SupergraphCreator { previous_cache: Option, count: Option, experimental_reuse_query_plans: bool, - experimental_pql_prewarm: bool, + experimental_pql_prewarm: &PersistedQueriesPrewarmQueryPlanCache, ) { self.query_planner_service .warm_up( diff --git a/docs/source/configuration/in-memory-caching.mdx b/docs/source/configuration/in-memory-caching.mdx index 000fade2e5..e90f6c1d3a 100644 --- a/docs/source/configuration/in-memory-caching.mdx +++ b/docs/source/configuration/in-memory-caching.mdx @@ -48,9 +48,7 @@ supergraph: When loading a new schema, a query plan might change for some queries, so cached query plans cannot be reused. -To prevent increased latency upon query plan cache invalidation, the router precomputes query plans for: -* The most used queries from the cache. -* The entire list of persisted queries. +To prevent increased latency upon query plan cache invalidation, the router precomputes query plans for the most used queries from the cache when a new schema is loaded. Precomputed plans will be cached before the router switches traffic over to the new schema. @@ -63,6 +61,8 @@ supergraph: warmed_up_queries: 100 ``` +(In addition, the router can use the contents of the [persisted query list](./persisted-queries) to prewarm the cache. By default, it does this when loading a new schema but not on startup; you can [configure](./persisted-queries#persisted-queries#experimental_prewarm_query_plan_cache) it to change either of these defaults.) + To get more information on the planning and warm-up process use the following metrics (where `` can be `redis` for distributed cache or `memory`): * counters: diff --git a/docs/source/configuration/persisted-queries.mdx b/docs/source/configuration/persisted-queries.mdx index bb2ba65d5e..0caabc7d1d 100644 --- a/docs/source/configuration/persisted-queries.mdx +++ b/docs/source/configuration/persisted-queries.mdx @@ -76,12 +76,14 @@ If used with the [`safelist`](#safelist) option, the router logs unregistered an -Adding `experimental_prewarm_query_plan_cache: true` to `persisted_queries` configures the router to prewarm the query plan cache on startup using the persisted query list. All subsequent requests benefit from the warmed cache, reducing latency and enhancing performance. +By default, the router [prewarms the query plan cache](./in-memory-caching#cache-warm-up) using all operations on the PQL when a new schema is loaded, but not at startup. Using the `experimental_prewarm_query_plan_cache` option, you can tell the router to prewarm the cache using the PQL on startup as well, or tell it not to prewarm the cache when reloading the schema. (This does not affect whether the router prewarms the query plan cache with recently-used operations from its in-memory cache.) Prewarming the cache means can reduce request latency by ensuring that operations are pre-planned when requests are received, but can make startup or schema reloads slower. ```yaml title="router.yaml" persisted_queries: enabled: true - experimental_prewarm_query_plan_cache: true # default: false + experimental_prewarm_query_plan_cache: + on_startup: true # default: false + on_reload: false # default: true ``` #### `experimental_local_manifests` From f2edf30ee0aebd583dc97dac316ee8f56933fbcd Mon Sep 17 00:00:00 2001 From: Iryna Shestak Date: Thu, 12 Sep 2024 11:42:44 +0200 Subject: [PATCH 17/56] feat(dual-qp): record comparison duration metrics (#5946) --- .../src/query_planner/bridge_query_planner.rs | 22 +++++---- .../src/query_planner/dual_query_planner.rs | 48 ++++++++++++++++++- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 040ec45a33..c8853670fc 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -65,7 +65,7 @@ use crate::spec::SpecError; use crate::Configuration; pub(crate) const RUST_QP_MODE: &str = "rust"; -const JS_QP_MODE: &str = "js"; +pub(crate) const JS_QP_MODE: &str = "js"; const UNSUPPORTED_CONTEXT: &str = "context"; const UNSUPPORTED_OVERRIDES: &str = "overrides"; const UNSUPPORTED_FED1: &str = "fed1"; @@ -272,7 +272,8 @@ impl PlannerMode { let result = js.plan(filtered_query, operation, plan_options).await; - metric_query_planning_plan_duration(JS_QP_MODE, start); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_duration(JS_QP_MODE, elapsed); let mut success = result .map_err(QueryPlannerError::RouterBridgeError)? @@ -296,7 +297,8 @@ impl PlannerMode { .and_then(|operation| rust.build_query_plan(&doc.executable, operation)) .map_err(|e| QueryPlannerError::FederationError(e.to_string())); - metric_query_planning_plan_duration(RUST_QP_MODE, start); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); let plan = result?; @@ -333,7 +335,8 @@ impl PlannerMode { .plan(filtered_query, operation.clone(), plan_options) .await; - metric_query_planning_plan_duration(JS_QP_MODE, start); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_duration(JS_QP_MODE, elapsed); let mut js_result = result .map_err(QueryPlannerError::RouterBridgeError)? @@ -350,6 +353,7 @@ impl PlannerMode { BothModeComparisonJob { rust_planner: rust.clone(), + js_duration: elapsed, document: doc.executable.clone(), operation_name: operation, // Exclude usage reporting from the Result sent for comparison @@ -1175,11 +1179,11 @@ pub fn render_diff(differences: &[diff::Result<&str>]) -> String { output } -pub(crate) fn metric_query_planning_plan_duration(planner: &'static str, start: Instant) { +pub(crate) fn metric_query_planning_plan_duration(planner: &'static str, elapsed: f64) { f64_histogram!( "apollo.router.query_planning.plan.duration", "Duration of the query planning.", - start.elapsed().as_secs_f64(), + elapsed, "planner" = planner ); } @@ -1830,7 +1834,8 @@ mod tests { #[test] fn test_metric_query_planning_plan_duration() { let start = Instant::now(); - metric_query_planning_plan_duration(RUST_QP_MODE, start); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); assert_histogram_exists!( "apollo.router.query_planning.plan.duration", f64, @@ -1838,7 +1843,8 @@ mod tests { ); let start = Instant::now(); - metric_query_planning_plan_duration(JS_QP_MODE, start); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_duration(JS_QP_MODE, elapsed); assert_histogram_exists!( "apollo.router.query_planning.plan.duration", f64, diff --git a/apollo-router/src/query_planner/dual_query_planner.rs b/apollo-router/src/query_planner/dual_query_planner.rs index 6a880cf538..e368582a97 100644 --- a/apollo-router/src/query_planner/dual_query_planner.rs +++ b/apollo-router/src/query_planner/dual_query_planner.rs @@ -22,6 +22,7 @@ use super::FlattenNode; use crate::error::format_bridge_errors; use crate::executable::USING_CATCH_UNWIND; use crate::query_planner::bridge_query_planner::metric_query_planning_plan_duration; +use crate::query_planner::bridge_query_planner::JS_QP_MODE; use crate::query_planner::bridge_query_planner::RUST_QP_MODE; use crate::query_planner::convert::convert_root_query_plan_node; use crate::query_planner::render_diff; @@ -38,6 +39,7 @@ const WORKER_THREAD_COUNT: usize = 1; pub(crate) struct BothModeComparisonJob { pub(crate) rust_planner: Arc, + pub(crate) js_duration: f64, pub(crate) document: Arc>, pub(crate) operation_name: Option, pub(crate) js_result: Result>>, @@ -88,7 +90,11 @@ impl BothModeComparisonJob { // No question mark operator or macro from here … let result = self.rust_planner.build_query_plan(&self.document, name); - metric_query_planning_plan_duration(RUST_QP_MODE, start); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); + + metric_query_planning_plan_both_comparison_duration(RUST_QP_MODE, elapsed); + metric_query_planning_plan_both_comparison_duration(JS_QP_MODE, self.js_duration); // … to here, so the thread can only eiher reach here or panic. // We unset USING_CATCH_UNWIND in both cases. @@ -169,6 +175,18 @@ impl BothModeComparisonJob { } } +pub(crate) fn metric_query_planning_plan_both_comparison_duration( + planner: &'static str, + elapsed: f64, +) { + f64_histogram!( + "apollo.router.operations.query_planner.both.duration", + "Comparing JS v.s. Rust query plan duration.", + elapsed, + "planner" = planner + ); +} + // Specific comparison functions pub struct MatchFailure { @@ -819,3 +837,31 @@ mod ast_comparison_tests { assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); } } + +#[cfg(test)] +mod tests { + use std::time::Instant; + + use super::*; + + #[test] + fn test_metric_query_planning_plan_both_comparison_duration() { + let start = Instant::now(); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_both_comparison_duration(RUST_QP_MODE, elapsed); + assert_histogram_exists!( + "apollo.router.operations.query_planner.both.duration", + f64, + "planner" = "rust" + ); + + let start = Instant::now(); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_both_comparison_duration(JS_QP_MODE, elapsed); + assert_histogram_exists!( + "apollo.router.operations.query_planner.both.duration", + f64, + "planner" = "js" + ); + } +} From 931f0c2b442268366a85ac65f177823029ac222e Mon Sep 17 00:00:00 2001 From: Iryna Shestak Date: Thu, 12 Sep 2024 16:41:54 +0200 Subject: [PATCH 18/56] chore(tests): use fed2 supergraphs in tests (#5861) Co-authored-by: Geoffroy Couprie Co-authored-by: Simon Sapin --- .../benches/fixtures/supergraph.graphql | 127 +++-- .../benches/deeply_nested/supergraph.graphql | 43 +- .../benches/huge_requests/supergraph.graphql | 43 +- apollo-router/src/configuration/tests.rs | 47 +- apollo-router/src/context/mod.rs | 22 +- apollo-router/src/json_ext.rs | 15 +- .../src/plugins/authorization/tests.rs | 42 +- apollo-router/src/plugins/cache/tests.rs | 48 +- ...y_plan__tests__it_expose_query_plan-2.snap | 6 +- ...ery_plan__tests__it_expose_query_plan.snap | 6 +- .../src/query_planner/bridge_query_planner.rs | 4 +- apollo-router/src/query_planner/rewrites.rs | 31 +- apollo-router/src/query_planner/selection.rs | 62 ++- .../testdata/defer_schema.graphql | 75 ++- .../src/query_planner/testdata/schema.graphql | 457 ++++++++++-------- apollo-router/src/query_planner/tests.rs | 29 +- apollo-router/src/router_factory.rs | 3 +- apollo-router/src/services/http/tests.rs | 83 ++-- .../src/services/supergraph/tests.rs | 313 ++++-------- apollo-router/src/spec/query/tests.rs | 117 +++-- apollo-router/src/spec/schema.rs | 49 +- .../src/testdata/a_b_supergraph.graphql | 60 +++ .../src/testdata/contract_schema.graphql | 117 ++++- .../testdata/inaccessible_on_non_core.graphql | 63 ++- ...al_supergraph_missing_subgraph_url.graphql | 31 ++ .../src/testdata/invalid_supergraph.graphql | 111 +++-- .../testdata/minimal_fed1_supergraph.graphql | 32 ++ .../testdata/minimal_fed2_supergraph.graphql | 44 -- ...minimal_local_inventory_supergraph.graphql | 36 ++ .../src/testdata/minimal_supergraph.graphql | 49 +- .../src/testdata/orga_supergraph.graphql | 79 +++ .../src/testdata/starstuff@current.graphql | 135 ++++-- apollo-router/src/testdata/supergraph.graphql | 109 +++-- .../testdata/supergraph_missing_name.graphql | 128 +++-- apollo-router/src/uplink/testdata/oss.graphql | 54 ++- apollo-router/testing_schema.graphql | 109 +++-- apollo-router/tests/fixtures/accounts.graphql | 15 +- .../tests/fixtures/inventory.graphql | 16 +- apollo-router/tests/fixtures/products.graphql | 13 +- apollo-router/tests/fixtures/reviews.graphql | 24 +- .../tests/fixtures/supergraph.graphql | 141 ++++-- .../tests/integration/query_planner.rs | 14 +- apollo-router/tests/integration/redis.rs | 99 +++- apollo-router/tests/samples/README.md | 2 +- .../samples/basic/query1/supergraph.graphql | 132 +++-- .../samples/basic/query2/supergraph.graphql | 135 ++++-- .../supergraph.graphql | 123 +++-- .../supergraph.graphql | 128 +++-- .../supergraph.graphql | 129 +++-- .../entity-cache/private/supergraph.graphql | 142 ++++-- .../query-planning-redis/supergraph.graphql | 143 ++++-- ..._tests__starstuff_supergraph_is_valid.snap | 142 +++--- docs/source/quickstart.mdx | 153 ++++-- examples/graphql/federation2.graphql | 134 ----- examples/graphql/local.graphql | 98 ++-- examples/graphql/supergraph-fed1.graphql | 90 ++++ examples/graphql/supergraph-fed2.graphql | 98 ---- examples/graphql/supergraph.graphql | 141 ++++-- fuzz/supergraph-fed2.graphql | 141 ------ fuzz/supergraph.graphql | 167 +++++-- 60 files changed, 3039 insertions(+), 2160 deletions(-) create mode 100644 apollo-router/src/testdata/a_b_supergraph.graphql create mode 100644 apollo-router/src/testdata/invalid_minimal_supergraph_missing_subgraph_url.graphql create mode 100644 apollo-router/src/testdata/minimal_fed1_supergraph.graphql delete mode 100644 apollo-router/src/testdata/minimal_fed2_supergraph.graphql create mode 100644 apollo-router/src/testdata/minimal_local_inventory_supergraph.graphql create mode 100644 apollo-router/src/testdata/orga_supergraph.graphql delete mode 100644 examples/graphql/federation2.graphql create mode 100644 examples/graphql/supergraph-fed1.graphql delete mode 100644 examples/graphql/supergraph-fed2.graphql delete mode 100644 fuzz/supergraph-fed2.graphql diff --git a/apollo-router-benchmarks/benches/fixtures/supergraph.graphql b/apollo-router-benchmarks/benches/fixtures/supergraph.graphql index d9d29019ee..d38a88745e 100644 --- a/apollo-router-benchmarks/benches/fixtures/supergraph.graphql +++ b/apollo-router-benchmarks/benches/fixtures/supergraph.graphql @@ -1,73 +1,128 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "http://localhost:4001/graphql") - INVENTORY @join__graph(name: "inventory" url: "http://localhost:4004/graphql") - PRODUCTS @join__graph(name: "products" url: "http://localhost:4003/graphql") - REVIEWS @join__graph(name: "reviews" url: "http://localhost:4002/graphql") + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) + recommendedProducts: [Product] @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/benches/deeply_nested/supergraph.graphql b/apollo-router/benches/deeply_nested/supergraph.graphql index e28b86e773..bf51cbf7ba 100644 --- a/apollo-router/benches/deeply_nested/supergraph.graphql +++ b/apollo-router/benches/deeply_nested/supergraph.graphql @@ -1,33 +1,60 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE directive @join__field( graph: join__Graph requires: join__FieldSet provides: join__FieldSet -) on FIELD_DEFINITION + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE directive @join__type( graph: join__Graph! key: join__FieldSet -) repeatable on OBJECT | INTERFACE + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet +scalar link__Import enum join__Graph { SUBGRAPH_1 @join__graph(name: "subgraph_1", url: "http://127.0.0.1:44168/") } +enum link__Purpose { + SECURITY + EXECUTION +} + type Query { value: Int! next: Query diff --git a/apollo-router/benches/huge_requests/supergraph.graphql b/apollo-router/benches/huge_requests/supergraph.graphql index d5f3f94dba..a6ed81a5e3 100644 --- a/apollo-router/benches/huge_requests/supergraph.graphql +++ b/apollo-router/benches/huge_requests/supergraph.graphql @@ -1,34 +1,61 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE directive @join__field( graph: join__Graph requires: join__FieldSet provides: join__FieldSet -) on FIELD_DEFINITION + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE directive @join__type( graph: join__Graph! key: join__FieldSet -) repeatable on OBJECT | INTERFACE + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet +scalar link__Import enum join__Graph { SUBGRAPH_1 @join__graph(name: "subgraph_1", url: "http://127.0.0.1:10141/") } +enum link__Purpose { + SECURITY + EXECUTION +} + type Query { unused: Int } diff --git a/apollo-router/src/configuration/tests.rs b/apollo-router/src/configuration/tests.rs index d1288df215..fb7acabf04 100644 --- a/apollo-router/src/configuration/tests.rs +++ b/apollo-router/src/configuration/tests.rs @@ -35,28 +35,7 @@ fn schema_generation() { #[test] fn routing_url_in_schema() { - let schema = r#" - schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") - { - query: Query - } - - type Query { - me: String - } - - directive @core(feature: String!) repeatable on SCHEMA - - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "http://localhost:4001/graphql") - INVENTORY @join__graph(name: "inventory" url: "http://localhost:4002/graphql") - PRODUCTS @join__graph(name: "products" url: "http://localhost:4003/graphql") - REVIEWS @join__graph(name: "reviews" url: "http://localhost:4004/graphql") - } - "#; + let schema = include_str!("../testdata/minimal_local_inventory_supergraph.graphql"); let schema = crate::spec::Schema::parse(schema, &Default::default()).unwrap(); let subgraphs: HashMap<&str, &Uri> = schema.subgraphs().map(|(k, v)| (k.as_str(), v)).collect(); @@ -87,28 +66,8 @@ fn routing_url_in_schema() { #[test] fn missing_subgraph_url() { - let schema_error = r#" - schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") - { - query: Query - } - - type Query { - me: String - } - - directive @core(feature: String!) repeatable on SCHEMA - - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - - enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "http://localhost:4001/graphql") - INVENTORY @join__graph(name: "inventory" url: "http://localhost:4002/graphql") - PRODUCTS @join__graph(name: "products" url: "http://localhost:4003/graphql") - REVIEWS @join__graph(name: "reviews" url: "") - }"#; + let schema_error = + include_str!("../testdata/invalid_minimal_supergraph_missing_subgraph_url.graphql"); let schema_error = crate::spec::Schema::parse(schema_error, &Default::default()) .expect_err("Must have an error because we have one missing subgraph routing url"); diff --git a/apollo-router/src/context/mod.rs b/apollo-router/src/context/mod.rs index 73794512a0..daade4ea5a 100644 --- a/apollo-router/src/context/mod.rs +++ b/apollo-router/src/context/mod.rs @@ -429,27 +429,7 @@ mod test { #[test] fn test_executable_document_access() { let c = Context::new(); - let schema = r#" - schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") - { - query: Query - } - type Query { - me: String - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - - enum join__Graph { - ACCOUNTS @join__graph(name:"accounts" url: "http://localhost:4001/graphql") - INVENTORY - @join__graph(name: "inventory", url: "http://localhost:4004/graphql") - PRODUCTS - @join__graph(name: "products" url: "http://localhost:4003/graphql") - REVIEWS @join__graph(name: "reviews" url: "http://localhost:4002/graphql") - }"#; + let schema = include_str!("../testdata/minimal_supergraph.graphql"); let schema = Schema::parse(schema, &Default::default()).unwrap(); let document = Query::parse_document("{ me }", None, &schema, &Configuration::default()).unwrap(); diff --git a/apollo-router/src/json_ext.rs b/apollo-router/src/json_ext.rs index 7e26929f24..eead336217 100644 --- a/apollo-router/src/json_ext.rs +++ b/apollo-router/src/json_ext.rs @@ -1164,16 +1164,23 @@ mod tests { Schema::parse( r#" schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query } - directive @core(feature: String!) repeatable on SCHEMA + directive @join__graph(name: String!, url: String!) on ENUM_VALUE + directive @link( url: String as: String for: link__Purpose import: [link__Import]) repeatable on SCHEMA + scalar link__Import enum join__Graph { - FAKE @join__graph(name:"fake" url: "http://localhost:4001/fake") + FAKE @join__graph(name:"fake" url: "http://localhost:4001/fake") + } + + enum link__Purpose { + SECURITY + EXECUTION } type Query { diff --git a/apollo-router/src/plugins/authorization/tests.rs b/apollo-router/src/plugins/authorization/tests.rs index 813533c184..d967125881 100644 --- a/apollo-router/src/plugins/authorization/tests.rs +++ b/apollo-router/src/plugins/authorization/tests.rs @@ -15,47 +15,7 @@ use crate::Context; use crate::MockedSubgraphs; use crate::TestHarness; -const SCHEMA: &str = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query -} -directive @core(feature: String!) repeatable on SCHEMA -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE -directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION -scalar join__FieldSet -enum join__Graph { - USER @join__graph(name: "user", url: "http://localhost:4001/graphql") - ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") -} -type Query { - currentUser: User @join__field(graph: USER) - orga(id: ID): Organization @join__field(graph: ORGA) -} -type User -@join__owner(graph: USER) -@join__type(graph: ORGA, key: "id") -@join__type(graph: USER, key: "id"){ - id: ID! - name: String - phone: String - activeOrganization: Organization -} -type Organization -@join__owner(graph: ORGA) -@join__type(graph: ORGA, key: "id") -@join__type(graph: USER, key: "id") { - id: ID - creatorUser: User - name: String - nonNullId: ID! - suborga: [Organization] -}"#; +const SCHEMA: &str = include_str!("../../testdata/orga_supergraph.graphql"); #[tokio::test] async fn authenticated_request() { diff --git a/apollo-router/src/plugins/cache/tests.rs b/apollo-router/src/plugins/cache/tests.rs index f9e83e3a81..64c96a0763 100644 --- a/apollo-router/src/plugins/cache/tests.rs +++ b/apollo-router/src/plugins/cache/tests.rs @@ -23,53 +23,7 @@ use crate::Context; use crate::MockedSubgraphs; use crate::TestHarness; -const SCHEMA: &str = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query - subscription: Subscription - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - scalar join__FieldSet - enum join__Graph { - USER @join__graph(name: "user", url: "http://localhost:4001/graphql") - ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") - } - type Query { - currentUser: User @join__field(graph: USER) - } - - type Subscription @join__type(graph: USER) { - userWasCreated: User - } - - type User - @join__owner(graph: USER) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id"){ - id: ID! - name: String - activeOrganization: Organization - allOrganizations: [Organization] - } - type Organization - @join__owner(graph: ORGA) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id") { - id: ID - creatorUser: User - name: String - nonNullId: ID! - suborga: [Organization] - }"#; - +const SCHEMA: &str = include_str!("../../testdata/orga_supergraph.graphql"); #[derive(Debug)] pub(crate) struct MockStore { map: Arc>>, 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 e914049664..b8130ecf59 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 @@ -69,7 +69,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "39cac6386a951cd4dbdfc9c91d7d24cc1061481ab03b72c483422446e09cba32", + "schemaAwareHash": "c595a39efeab9494c75a29de44ec4748c1741ddb96e1833e99139b058aa9da84", "authorization": { "is_authenticated": false, "scopes": [], @@ -109,7 +109,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "ee6ac550117eed7d8fcaf66c83fd5177bf03a9d5761f484e2664ea4e66149127", + "schemaAwareHash": "7054d7662e20905b01d6f937e6b588ed422e0e79de737c98e3d51b6dc610179f", "authorization": { "is_authenticated": false, "scopes": [], @@ -200,7 +200,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "66c61f60e730b77cd0a58908fee01dc7a0742c47e9f847037e01297d37918821", + "schemaAwareHash": "bff0ce0cfd6e2830949c59ae26f350d06d76150d6041b08c3d0c4384bc20b271", "authorization": { "is_authenticated": false, "scopes": [], 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 e914049664..b8130ecf59 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 @@ -69,7 +69,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "39cac6386a951cd4dbdfc9c91d7d24cc1061481ab03b72c483422446e09cba32", + "schemaAwareHash": "c595a39efeab9494c75a29de44ec4748c1741ddb96e1833e99139b058aa9da84", "authorization": { "is_authenticated": false, "scopes": [], @@ -109,7 +109,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "ee6ac550117eed7d8fcaf66c83fd5177bf03a9d5761f484e2664ea4e66149127", + "schemaAwareHash": "7054d7662e20905b01d6f937e6b588ed422e0e79de737c98e3d51b6dc610179f", "authorization": { "is_authenticated": false, "scopes": [], @@ -200,7 +200,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "66c61f60e730b77cd0a58908fee01dc7a0742c47e9f847037e01297d37918821", + "schemaAwareHash": "bff0ce0cfd6e2830949c59ae26f350d06d76150d6041b08c3d0c4384bc20b271", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index c8853670fc..11dd9c2b08 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -1259,7 +1259,7 @@ mod tests { #[test(tokio::test)] async fn federation_versions() { async { - let sdl = include_str!("../testdata/minimal_supergraph.graphql"); + let sdl = include_str!("../testdata/minimal_fed1_supergraph.graphql"); let config = Arc::default(); let schema = Schema::parse(sdl, &config).unwrap(); let _planner = BridgeQueryPlanner::new(schema.into(), config, None, None) @@ -1276,7 +1276,7 @@ mod tests { .await; async { - let sdl = include_str!("../testdata/minimal_fed2_supergraph.graphql"); + let sdl = include_str!("../testdata/minimal_supergraph.graphql"); let config = Arc::default(); let schema = Schema::parse(sdl, &config).unwrap(); let _planner = BridgeQueryPlanner::new(schema.into(), config, None, None) diff --git a/apollo-router/src/query_planner/rewrites.rs b/apollo-router/src/query_planner/rewrites.rs index 49733e9ad1..878c18da04 100644 --- a/apollo-router/src/query_planner/rewrites.rs +++ b/apollo-router/src/query_planner/rewrites.rs @@ -116,36 +116,7 @@ mod tests { // The schema is not used for the tests // but we need a valid one - const SCHEMA: &str = r#" - schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") - { - query: Query - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - - enum join__Graph { - FAKE @join__graph(name:"fake" url: "http://localhost:4001/fake") - } - - type Query { - i: [I] - } - - interface I { - x: Int - } - - type A implements I { - x: Int - } - - type B { - y: Int - } - "#; + const SCHEMA: &str = include_str!("../testdata/minimal_supergraph.graphql"); #[test] fn test_key_renamer_object() { diff --git a/apollo-router/src/query_planner/selection.rs b/apollo-router/src/query_planner/selection.rs index e2c5e9b013..810c1c1ac5 100644 --- a/apollo-router/src/query_planner/selection.rs +++ b/apollo-router/src/query_planner/selection.rs @@ -367,7 +367,7 @@ mod tests { assert_eq!( select!( with_supergraph_boilerplate( - "type Query { me: String } type Author { name: String } type Reviewer { name: String } \ + "type Query @join__type(graph: TEST) { me: String @join__field(graph: TEST) } type Author { name: String } type Reviewer { name: String } \ union User = Author | Reviewer" ), bjson!({"__typename": "Author", "id":2, "name":"Bob", "job":{"name":"astronaut"}}), @@ -400,7 +400,7 @@ mod tests { #[test] fn test_array() { let schema = with_supergraph_boilerplate( - "type Query { me: String } + "type Query @join__type(graph: TEST){ me: String @join__field(graph: TEST) } type MainObject { mainObjectList: [SubObject] } type SubObject { key: String name: String }", ); @@ -471,7 +471,7 @@ mod tests { #[test] fn test_execute_selection_set_abstract_types() { let schema = with_supergraph_boilerplate( - "type Query { hello: String } + "type Query @join__type(graph: TEST){ hello: String @join__field(graph: TEST)} type Entity { id: Int! nestedUnion: NestedUnion @@ -743,16 +743,62 @@ mod tests { "{}\n{}", r#" schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") { - query: Query + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query } - directive @core(feature: String!) repeatable on SCHEMA + + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + + directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean + ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + directive @join__graph(name: String!, url: String!) on ENUM_VALUE + + directive @join__implements( + graph: join__Graph! + interface: String! + ) repeatable on OBJECT | INTERFACE + + directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false + ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + + directive @join__unionMember( + graph: join__Graph! + member: String! + ) repeatable on UNION + + directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] + ) repeatable on SCHEMA + + scalar join__FieldSet + enum join__Graph { TEST @join__graph(name: "test", url: "http://localhost:4001/graphql") } - + + scalar link__Import + + enum link__Purpose { + SECURITY + EXECUTION + } "#, content ) diff --git a/apollo-router/src/query_planner/testdata/defer_schema.graphql b/apollo-router/src/query_planner/testdata/defer_schema.graphql index 54c0b4db9b..75dc482375 100644 --- a/apollo-router/src/query_planner/testdata/defer_schema.graphql +++ b/apollo-router/src/query_planner/testdata/defer_schema.graphql @@ -1,39 +1,72 @@ schema -@core(feature: "https://specs.apollo.dev/core/v0.1"), -@core(feature: "https://specs.apollo.dev/join/v0.1") -{ - query: Query + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +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 @stream on FIELD +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 directive @transform(from: String!) on FIELD +directive @stream on FIELD + scalar join__FieldSet enum join__Graph { -X @join__graph(name: "X" url: "http://X") -Y @join__graph(name: "Y" url: "http://Y") + X @join__graph(name: "X", url: "http://X") + Y @join__graph(name: "Y", url: "http://Y") } -type Query { - t: T @join__field(graph: X) +scalar link__Import + +enum link__Purpose { + SECURITY + EXECUTION } -type T -@join__owner(graph: X) -@join__type(graph: X, key: "id") { - id: ID @join__field(graph: X) - x: String @join__field(graph: X) - y: String @join__field(graph: Y) -} \ No newline at end of file +type Query @join__type(graph: X) @join__type(graph: Y) { + t: T @join__field(graph: X) +} + +type T @join__type(graph: X, key: "id") { + id: ID @join__field(graph: X) + x: String @join__field(graph: X) + y: String @join__field(graph: Y) +} diff --git a/apollo-router/src/query_planner/testdata/schema.graphql b/apollo-router/src/query_planner/testdata/schema.graphql index f019d0dd3f..ce450759f8 100644 --- a/apollo-router/src/query_planner/testdata/schema.graphql +++ b/apollo-router/src/query_planner/testdata/schema.graphql @@ -1,272 +1,341 @@ schema -@core(feature: "https://specs.apollo.dev/core/v0.1"), -@core(feature: "https://specs.apollo.dev/join/v0.1") -{ - query: Query - mutation: Mutation + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query + mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +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 -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +scalar join__FieldSet + +enum join__Graph { + ACCOUNTS @join__graph(name: "accounts", url: "http://accounts") + BOOKS @join__graph(name: "books", url: "http://books") + DOCUMENTS @join__graph(name: "documents", url: "http://documents") + INVENTORY @join__graph(name: "inventory", url: "http://inventory") + PRODUCT @join__graph(name: "product", url: "http://products") + REVIEWS @join__graph(name: "reviews", url: "http://reviews") +} + +scalar link__Import +enum link__Purpose { + SECURITY + EXECUTION +} directive @stream on FIELD directive @transform(from: String!) on FIELD -union AccountType = PasswordAccount | SMSAccount +union AccountType @join__type(graph: ACCOUNTS) = PasswordAccount | SMSAccount -type Amazon { -referrer: String +type Amazon @join__type(graph: PRODUCT, key: "referrer") { + referrer: String } -union Body = Image | Text +union Body @join__type(graph: DOCUMENTS) = Image | Text type Book implements Product -@join__owner(graph: BOOKS) -@join__type(graph: BOOKS, key: "isbn") -@join__type(graph: INVENTORY, key: "isbn") -@join__type(graph: PRODUCT, key: "isbn") -@join__type(graph: REVIEWS, key: "isbn") -{ -isbn: String! @join__field(graph: BOOKS) -title: String @join__field(graph: BOOKS) -year: Int @join__field(graph: BOOKS) -similarBooks: [Book]! @join__field(graph: BOOKS) -metadata: [MetadataOrError] @join__field(graph: BOOKS) -inStock: Boolean @join__field(graph: INVENTORY) -isCheckedOut: Boolean @join__field(graph: INVENTORY) -upc: String! @join__field(graph: PRODUCT) -sku: String! @join__field(graph: PRODUCT) -name(delimeter: String = " "): String @join__field(graph: PRODUCT, requires: "title year") -price: String @join__field(graph: PRODUCT) -details: ProductDetailsBook @join__field(graph: PRODUCT) -reviews: [Review] @join__field(graph: REVIEWS) -relatedReviews: [Review!]! @join__field(graph: REVIEWS, requires: "similarBooks{isbn}") + @join__type(graph: BOOKS, key: "isbn") + @join__type(graph: INVENTORY, key: "isbn") + @join__type(graph: PRODUCT, key: "isbn") + @join__type(graph: REVIEWS, key: "isbn") { + isbn: String! + @join__field(graph: BOOKS) + @join__field(graph: REVIEWS, external: true) + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCT, external: true) + title: String + @join__field(graph: BOOKS) + @join__field(graph: PRODUCT, external: true) + year: Int + @join__field(graph: BOOKS) + @join__field(graph: PRODUCT, external: true) + similarBooks: [Book]! + @join__field(graph: BOOKS) + @join__field(graph: REVIEWS, external: true) + metadata: [MetadataOrError] @join__field(graph: BOOKS) + inStock: Boolean @join__field(graph: INVENTORY) + isCheckedOut: Boolean @join__field(graph: INVENTORY) + upc: String! @join__field(graph: PRODUCT) + sku: String! @join__field(graph: PRODUCT) + name(delimeter: String = " "): String + @join__field(graph: PRODUCT, requires: "title year") + price: String @join__field(graph: PRODUCT) + details: ProductDetailsBook @join__field(graph: PRODUCT) + reviews: [Review] @join__field(graph: REVIEWS) + relatedReviews: [Review!]! + @join__field(graph: REVIEWS, requires: "similarBooks{isbn}") } -union Brand = Ikea | Amazon +union Brand @join__type(graph: PRODUCT) = Ikea | Amazon type Car implements Vehicle -@join__owner(graph: PRODUCT) -@join__type(graph: PRODUCT, key: "id") -@join__type(graph: REVIEWS, key: "id") -{ -id: String! @join__field(graph: PRODUCT) -description: String @join__field(graph: PRODUCT) -price: String @join__field(graph: PRODUCT) -retailPrice: String @join__field(graph: REVIEWS, requires: "price") + @join__type(graph: PRODUCT, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: String! @join__field(graph: PRODUCT) @join__field(graph: REVIEWS) + description: String @join__field(graph: PRODUCT) + price: String + @join__field(graph: PRODUCT) + @join__field(graph: REVIEWS, external: true) + retailPrice: String @join__field(graph: REVIEWS, requires: "price") } -type Error { -code: Int -message: String +type Error + @join__type(graph: REVIEWS, key: "code") + @join__type(graph: PRODUCT, key: "code") + @join__type(graph: BOOKS, key: "code") { + code: Int + message: String } type Furniture implements Product -@join__owner(graph: PRODUCT) -@join__type(graph: PRODUCT, key: "upc") -@join__type(graph: PRODUCT, key: "sku") -@join__type(graph: INVENTORY, key: "sku") -@join__type(graph: REVIEWS, key: "upc") -{ -upc: String! @join__field(graph: PRODUCT) -sku: String! @join__field(graph: PRODUCT) -name: String @join__field(graph: PRODUCT) -price: String @join__field(graph: PRODUCT) -brand: Brand @join__field(graph: PRODUCT) -metadata: [MetadataOrError] @join__field(graph: PRODUCT) -details: ProductDetailsFurniture @join__field(graph: PRODUCT) -inStock: Boolean @join__field(graph: INVENTORY) -isHeavy: Boolean @join__field(graph: INVENTORY) -reviews: [Review] @join__field(graph: REVIEWS) + @join__type(graph: PRODUCT, key: "upc") + @join__type(graph: PRODUCT, key: "sku") + @join__type(graph: INVENTORY, key: "sku") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + @join__field(graph: PRODUCT) + @join__field(graph: REVIEWS, external: true) + sku: String! + @join__field(graph: PRODUCT) + @join__field(graph: INVENTORY, external: true) + name: String @join__field(graph: PRODUCT) + price: String @join__field(graph: PRODUCT) + brand: Brand @join__field(graph: PRODUCT) + metadata: [MetadataOrError] @join__field(graph: PRODUCT) + details: ProductDetailsFurniture @join__field(graph: PRODUCT) + inStock: Boolean @join__field(graph: INVENTORY) + isHeavy: Boolean @join__field(graph: INVENTORY) + reviews: [Review] @join__field(graph: REVIEWS) } -type Ikea { -asile: Int +type Ikea @join__type(graph: PRODUCT, key: "asile") { + asile: Int } -type Image implements NamedObject { -name: String! -attributes: ImageAttributes! +type Image implements NamedObject @join__type(graph: DOCUMENTS, key: "name") { + name: String! + attributes: ImageAttributes! } -type ImageAttributes { -url: String! +type ImageAttributes @join__type(graph: DOCUMENTS, key: "url") { + url: String! } -scalar join__FieldSet - -enum join__Graph { -ACCOUNTS @join__graph(name: "accounts" url: "http://accounts") -BOOKS @join__graph(name: "books" url: "http://books") -DOCUMENTS @join__graph(name: "documents" url: "http://documents") -INVENTORY @join__graph(name: "inventory" url: "http://inventory") -PRODUCT @join__graph(name: "product" url: "http://products") -REVIEWS @join__graph(name: "reviews" url: "http://reviews") -} - -type KeyValue { -key: String! -value: String! +type KeyValue + @join__type(graph: REVIEWS, key: "key") + @join__type(graph: PRODUCT, key: "key") + @join__type(graph: BOOKS, key: "key") { + key: String! + value: String! } type Library -@join__owner(graph: BOOKS) -@join__type(graph: BOOKS, key: "id") -@join__type(graph: ACCOUNTS, key: "id") -{ -id: ID! @join__field(graph: BOOKS) -name: String @join__field(graph: BOOKS) -userAccount(id: ID! = 1): User @join__field(graph: ACCOUNTS, requires: "name") + @join__type(graph: BOOKS, key: "id") + @join__type(graph: ACCOUNTS, key: "id") { + id: ID! + name: String + @join__field(graph: BOOKS) + @join__field(graph: ACCOUNTS, external: true) + userAccount(id: ID! = 1): User @join__field(graph: ACCOUNTS, requires: "name") } -union MetadataOrError = KeyValue | Error - -type Mutation { -login(username: String!, password: String!): User @join__field(graph: ACCOUNTS) -reviewProduct(upc: String!, body: String!): Product @join__field(graph: REVIEWS) -updateReview(review: UpdateReviewInput!): Review @join__field(graph: REVIEWS) -deleteReview(id: ID!): Boolean @join__field(graph: REVIEWS) +union MetadataOrError + @join__type(graph: REVIEWS) + @join__type(graph: PRODUCT) + @join__type(graph: BOOKS) + @join__type(graph: REVIEWS) + @join__type(graph: PRODUCT) + @join__type(graph: BOOKS) = + KeyValue + | Error + +type Mutation @join__type(graph: ACCOUNTS) @join__type(graph: REVIEWS) { + login(username: String!, password: String!): User + @join__field(graph: ACCOUNTS) + reviewProduct(upc: String!, body: String!): Product + @join__field(graph: REVIEWS) + updateReview(review: UpdateReviewInput!): Review @join__field(graph: REVIEWS) + deleteReview(id: ID!): Boolean @join__field(graph: REVIEWS) } -type Name { -first: String -last: String +type Name @join__type(graph: ACCOUNTS) { + first: String + last: String } -interface NamedObject { -name: String! +interface NamedObject @join__type(graph: DOCUMENTS) { + name: String! } -type PasswordAccount -@join__owner(graph: ACCOUNTS) -@join__type(graph: ACCOUNTS, key: "email") -{ -email: String! @join__field(graph: ACCOUNTS) +type PasswordAccount @join__type(graph: ACCOUNTS, key: "email") { + email: String! @join__field(graph: ACCOUNTS) } -interface Product { -upc: String! -sku: String! -name: String -price: String -details: ProductDetails -inStock: Boolean -reviews: [Review] +interface Product @join__type(graph: PRODUCT) @join__type(graph: REVIEWS) { + upc: String! + sku: String! + name: String + price: String + details: ProductDetails + inStock: Boolean + reviews: [Review] } -interface ProductDetails { -country: String +interface ProductDetails + @join__type(graph: PRODUCT) + @join__type(graph: REVIEWS) { + country: String } -type ProductDetailsBook implements ProductDetails { -country: String -pages: Int +type ProductDetailsBook implements ProductDetails @join__type(graph: PRODUCT) { + country: String + pages: Int } -type ProductDetailsFurniture implements ProductDetails { -country: String -color: String +type ProductDetailsFurniture implements ProductDetails + @join__type(graph: PRODUCT) { + country: String + color: String } -type Query { -user(id: ID!): User @join__field(graph: ACCOUNTS) -me: User @join__field(graph: ACCOUNTS) -book(isbn: String!): Book @join__field(graph: BOOKS) -books: [Book] @join__field(graph: BOOKS) -library(id: ID!): Library @join__field(graph: BOOKS) -body: Body! @join__field(graph: DOCUMENTS) -product(upc: String!): Product @join__field(graph: PRODUCT) -vehicle(id: String!): Vehicle @join__field(graph: PRODUCT) -topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCT) -topCars(first: Int = 5): [Car] @join__field(graph: PRODUCT) -topReviews(first: Int = 5): [Review] @join__field(graph: REVIEWS) +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: DOCUMENTS) + @join__type(graph: BOOKS) + @join__type(graph: PRODUCT) + @join__type(graph: REVIEWS) { + user(id: ID!): User @join__field(graph: ACCOUNTS) + me: User @join__field(graph: ACCOUNTS) + book(isbn: String!): Book @join__field(graph: BOOKS) + books: [Book] @join__field(graph: BOOKS) + library(id: ID!): Library @join__field(graph: BOOKS) + body: Body! @join__field(graph: DOCUMENTS) + product(upc: String!): Product @join__field(graph: PRODUCT) + vehicle(id: String!): Vehicle @join__field(graph: PRODUCT) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCT) + topCars(first: Int = 5): [Car] @join__field(graph: PRODUCT) + topReviews(first: Int = 5): [Review] @join__field(graph: REVIEWS) } type Review -@join__owner(graph: REVIEWS) -@join__type(graph: REVIEWS, key: "id") -{ -id: ID! @join__field(graph: REVIEWS) -body(format: Boolean = false): String @join__field(graph: REVIEWS) -author: User @join__field(graph: REVIEWS, provides: "username") -product: Product @join__field(graph: REVIEWS) -metadata: [MetadataOrError] @join__field(graph: REVIEWS) + @join__type(graph: REVIEWS, key: "id") + @join__type(graph: PRODUCT, key: "id") { + id: ID! @join__field(graph: REVIEWS) + body(format: Boolean = false): String @join__field(graph: REVIEWS) + author: User @join__field(graph: REVIEWS, provides: "username") + product: Product @join__field(graph: REVIEWS) + metadata: [MetadataOrError] @join__field(graph: REVIEWS) } -type SMSAccount -@join__owner(graph: ACCOUNTS) -@join__type(graph: ACCOUNTS, key: "number") -{ -number: String @join__field(graph: ACCOUNTS) +type SMSAccount @join__type(graph: ACCOUNTS, key: "number") { + number: String @join__field(graph: ACCOUNTS) } -type Text implements NamedObject { -name: String! -attributes: TextAttributes! +type Text implements NamedObject @join__type(graph: DOCUMENTS, key: "name") { + name: String! + attributes: TextAttributes! } -type TextAttributes { -bold: Boolean -text: String +type TextAttributes @join__type(graph: DOCUMENTS) { + bold: Boolean + text: String } -union Thing = Car | Ikea +union Thing @join__type(graph: PRODUCT) = Car | Ikea -input UpdateReviewInput { -id: ID! -body: String +input UpdateReviewInput @join__type(graph: REVIEWS) { + id: ID! + body: String } type User -@join__owner(graph: ACCOUNTS) -@join__type(graph: ACCOUNTS, key: "id") -@join__type(graph: ACCOUNTS, key: "username name{first last}") -@join__type(graph: INVENTORY, key: "id") -@join__type(graph: PRODUCT, key: "id") -@join__type(graph: REVIEWS, key: "id") -{ -id: ID! @join__field(graph: ACCOUNTS) -name: Name @join__field(graph: ACCOUNTS) -username: String @join__field(graph: ACCOUNTS) -birthDate(locale: String): String @join__field(graph: ACCOUNTS) -account: AccountType @join__field(graph: ACCOUNTS) -metadata: [UserMetadata] @join__field(graph: ACCOUNTS) -goodDescription: Boolean @join__field(graph: INVENTORY, requires: "metadata{description}") -vehicle: Vehicle @join__field(graph: PRODUCT) -thing: Thing @join__field(graph: PRODUCT) -reviews: [Review] @join__field(graph: REVIEWS) -numberOfReviews: Int! @join__field(graph: REVIEWS) -goodAddress: Boolean @join__field(graph: REVIEWS, requires: "metadata{address}") + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: ACCOUNTS, key: "username name{first last}") + @join__type(graph: INVENTORY, key: "id") + @join__type(graph: PRODUCT, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! + name: Name @join__field(graph: ACCOUNTS) + username: String @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS) + birthDate(locale: String): String @join__field(graph: ACCOUNTS) + account: AccountType @join__field(graph: ACCOUNTS) + metadata: [UserMetadata] + @join__field(graph: ACCOUNTS) + @join__field(graph: INVENTORY, external: true) + @join__field(graph: REVIEWS, external: true) + goodDescription: Boolean + @join__field(graph: INVENTORY, requires: "metadata{description}") + vehicle: Vehicle @join__field(graph: PRODUCT) + thing: Thing @join__field(graph: PRODUCT) + reviews: [Review] @join__field(graph: REVIEWS) + numberOfReviews: Int! @join__field(graph: REVIEWS) + goodAddress: Boolean + @join__field(graph: REVIEWS, requires: "metadata{address}") } -type UserMetadata { -name: String -address: String -description: String +type UserMetadata + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: REVIEWS) { + name: String + address: String + description: String } type Van implements Vehicle -@join__owner(graph: PRODUCT) -@join__type(graph: PRODUCT, key: "id") -@join__type(graph: REVIEWS, key: "id") -{ -id: String! @join__field(graph: PRODUCT) -description: String @join__field(graph: PRODUCT) -price: String @join__field(graph: PRODUCT) -retailPrice: String @join__field(graph: REVIEWS, requires: "price") + @join__type(graph: PRODUCT, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: String! + description: String @join__field(graph: PRODUCT) + price: String + @join__field(graph: PRODUCT) + @join__field(graph: REVIEWS, external: true) + retailPrice: String @join__field(graph: REVIEWS, requires: "price") } -interface Vehicle { -id: String! -description: String -price: String -retailPrice: String +interface Vehicle @join__type(graph: PRODUCT) { + id: String! + description: String + price: String + retailPrice: String } diff --git a/apollo-router/src/query_planner/tests.rs b/apollo-router/src/query_planner/tests.rs index cfd44d6d08..131dc7ba12 100644 --- a/apollo-router/src/query_planner/tests.rs +++ b/apollo-router/src/query_planner/tests.rs @@ -575,34 +575,7 @@ async fn defer_if_condition() { #[tokio::test] async fn dependent_mutations() { - let schema = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") - { - query: Query - mutation: Mutation - } - - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - scalar join__FieldSet - - enum join__Graph { - A @join__graph(name: "A" url: "http://localhost:4001") - B @join__graph(name: "B" url: "http://localhost:4004") - } - - type Mutation { - mutationA: Mutation @join__field(graph: A) - mutationB: Boolean @join__field(graph: B) - } - - type Query { - query: Boolean @join__field(graph: A) - }"#; + let schema = include_str!("../testdata/a_b_supergraph.graphql"); let query_plan: QueryPlan = QueryPlan { // generated from: diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs index fa2bde177f..0cf4bda19d 100644 --- a/apollo-router/src/router_factory.rs +++ b/apollo-router/src/router_factory.rs @@ -497,8 +497,7 @@ pub async fn create_test_service_factory_from_yaml(schema: &str, configuration: .await; assert_eq!( service.map(|_| ()).unwrap_err().to_string().as_str(), - r#"couldn't build Query Planner Service: couldn't instantiate query planner; invalid schema: schema validation errors: Error extracting subgraphs from the supergraph: this might be due to errors in subgraphs that were mistakenly ignored by federation 0.x versions but are rejected by federation 2. -Please try composing your subgraphs with federation 2: this should help precisely pinpoint the problems and, once fixed, generate a correct federation 2 supergraph. + r#"couldn't build Query Planner Service: couldn't instantiate query planner; invalid schema: schema validation errors: Unexpected error extracting subgraphs from the supergraph: this is either a bug, or the supergraph has been corrupted. Details: Error: Cannot find type "Review" in subgraph "products" diff --git a/apollo-router/src/services/http/tests.rs b/apollo-router/src/services/http/tests.rs index de14b8d64f..892dc30e1b 100644 --- a/apollo-router/src/services/http/tests.rs +++ b/apollo-router/src/services/http/tests.rs @@ -452,51 +452,7 @@ async fn test_compressed_request_response_body() { ); } -const SCHEMA: &str = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query - subscription: Subscription - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - scalar join__FieldSet - enum join__Graph { - USER @join__graph(name: "user", url: "http://localhost:4001/graphql") - ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") - } - type Query { - currentUser: User @join__field(graph: USER) - } - - type Subscription @join__type(graph: USER) { - userWasCreated: User - } - - type User - @join__owner(graph: USER) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id"){ - id: ID! - name: String - activeOrganization: Organization - } - type Organization - @join__owner(graph: ORGA) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id") { - id: ID - creatorUser: User - name: String - nonNullId: ID! - suborga: [Organization] - }"#; +const SCHEMA: &str = include_str!("../../testdata/orga_supergraph.graphql"); struct TestPlugin { started: Arc, @@ -553,29 +509,48 @@ async fn test_http_plugin_is_loaded() { fn make_schema(path: &str) -> String { r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/authenticated/v0.1", for: SECURITY) { query: Query } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE + directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + + 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 + } + scalar join__FieldSet enum join__Graph { USER @join__graph(name: "user", url: "unix://"#.to_string()+path+r#"") ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") } - type Query { + type Query + @join__type(graph: ORGA) + @join__type(graph: USER) + { currentUser: User @join__field(graph: USER) } type User - @join__owner(graph: USER) @join__type(graph: ORGA, key: "id") @join__type(graph: USER, key: "id"){ id: ID! diff --git a/apollo-router/src/services/supergraph/tests.rs b/apollo-router/src/services/supergraph/tests.rs index e624dd07a9..7a3d6765a1 100644 --- a/apollo-router/src/services/supergraph/tests.rs +++ b/apollo-router/src/services/supergraph/tests.rs @@ -18,51 +18,7 @@ use crate::Context; use crate::Notify; use crate::TestHarness; -const SCHEMA: &str = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query - subscription: Subscription - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - scalar join__FieldSet - enum join__Graph { - USER @join__graph(name: "user", url: "http://localhost:4001/graphql") - ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") - } - type Query { - currentUser: User @join__field(graph: USER) - } - - type Subscription @join__type(graph: USER) { - userWasCreated: User - } - - type User - @join__owner(graph: USER) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id"){ - id: ID! - name: String - activeOrganization: Organization - } - type Organization - @join__owner(graph: ORGA) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id") { - id: ID - creatorUser: User - name: String - nonNullId: ID! - suborga: [Organization] - }"#; +const SCHEMA: &str = include_str!("../../testdata/orga_supergraph.graphql"); #[tokio::test] async fn nullability_formatting() { @@ -289,52 +245,6 @@ fragment TestFragment on Query { #[tokio::test] async fn root_selection_skipped_with_other_fields() { - const SCHEMA: &str = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query - subscription: Subscription - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - scalar join__FieldSet - enum join__Graph { - USER @join__graph(name: "user", url: "http://localhost:4001/graphql") - ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") - } - type Query { - currentUser: User @join__field(graph: USER) - otherUser: User @join__field(graph: USER) - } - - type Subscription @join__type(graph: USER) { - userWasCreated: User - } - - type User - @join__owner(graph: USER) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id"){ - id: ID! - name: String - activeOrganization: Organization - } - type Organization - @join__owner(graph: ORGA) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id") { - id: ID - creatorUser: User - name: String - nonNullId: ID! - suborga: [Organization] - }"#; let subgraphs = MockedSubgraphs( [ ( @@ -2576,43 +2486,40 @@ async fn no_typename_on_interface() { .unwrap() .schema( r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query } - directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA - directive @join__field(graph: join__Graph, provides: join__FieldSet, requires: join__FieldSet) on FIELD_DEFINITION + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on INTERFACE | OBJECT + 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 - interface Animal { + interface Animal @join__type(graph: ANIMAL) { id: String! } - type Dog implements Animal { + type Dog implements Animal + @join__implements(graph: ANIMAL, interface: "Animal") + @join__type(graph: ANIMAL) + { id: String! name: String! } - type Query { + type Query @join__type(graph: ANIMAL) { animal: Animal! @join__field(graph: ANIMAL) dog: Dog! @join__field(graph: ANIMAL) } - enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION + scalar link__Import - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ + enum link__Purpose { SECURITY + EXECUTION } - scalar join__FieldSet enum join__Graph { @@ -2749,64 +2656,63 @@ async fn aliased_typename_on_fragments() { .unwrap() .schema( r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query } - directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA - directive @join__field(graph: join__Graph, provides: join__FieldSet, requires: join__FieldSet) on FIELD_DEFINITION + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT + 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 @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + + scalar link__Import - interface Animal { + enum link__Purpose { + SECURITY + EXECUTION + } + scalar join__FieldSet + + interface Animal + @join__type(graph: ANIMAL) + { id: String! } - type Dog implements Animal { + type Dog implements Animal + @join__implements(graph: ANIMAL, interface: "Animal") + @join__type(graph: ANIMAL) + { id: String! name: String! nickname: String! barkVolume: Int } - type Cat implements Animal { + type Cat implements Animal + @join__implements(graph: ANIMAL, interface: "Animal") + @join__type(graph: ANIMAL) + { id: String! name: String! nickname: String! meowVolume: Int } - type Query { + type Query @join__type(graph: ANIMAL){ animal: Animal! @join__field(graph: ANIMAL) dog: Dog! @join__field(graph: ANIMAL) } - enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION - - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY - } - union CatOrDog @join__type(graph: ANIMAL) @join__unionMember(graph: ANIMAL, member: "Dog") @join__unionMember(graph: ANIMAL, member: "Cat") = Cat | Dog - scalar join__FieldSet - enum join__Graph { ANIMAL @join__graph(name: "animal" url: "http://localhost:8080/query") } @@ -3097,18 +3003,10 @@ async fn interface_object_typename() { query: Query } - directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE - directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR - - directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION - directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA directive @owner( @@ -3126,14 +3024,7 @@ async fn interface_object_typename() { 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 } @@ -3292,40 +3183,50 @@ async fn interface_object_typename() { #[tokio::test] async fn fragment_reuse() { const SCHEMA: &str = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - scalar join__FieldSet - enum join__Graph { - USER @join__graph(name: "user", url: "http://localhost:4001/graphql") - ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") - } - type Query { - me: User @join__field(graph: USER) - } + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + directive @join__graph(name: String!, url: String!) on ENUM_VALUE + directive @join__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__implements( graph: join__Graph! interface: String!) repeatable on OBJECT | INTERFACE - type User - @join__owner(graph: USER) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id"){ - id: ID! - name: String - organizations: [Organization] @join__field(graph: ORGA) - } - type Organization - @join__owner(graph: ORGA) - @join__type(graph: ORGA, key: "id") { - id: ID - name: String + scalar link__Import + + enum link__Purpose { + SECURITY + EXECUTION + } + scalar join__FieldSet + + enum join__Graph { + USER @join__graph(name: "user", url: "http://localhost:4001/graphql") + ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") + } + + type Query + @join__type(graph: ORGA) + @join__type(graph: USER) + { + me: User @join__field(graph: USER) + } + + type User + @join__type(graph: ORGA, key: "id") + @join__type(graph: USER, key: "id") + { + id: ID! + name: String + organizations: [Organization] @join__field(graph: ORGA) + } + type Organization + @join__type(graph: ORGA, key: "id") + { + id: ID + name: String @join__field(graph: ORGA) }"#; let subgraphs = MockedSubgraphs([ @@ -3393,18 +3294,11 @@ async fn abstract_types_in_requires() { query: Query } - directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE - directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR - directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION - directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA scalar join__FieldSet @@ -3417,14 +3311,7 @@ async fn abstract_types_in_requires() { 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 } @@ -3555,28 +3442,34 @@ async fn abstract_types_in_requires() { } const ENUM_SCHEMA: &str = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + 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 + + scalar link__Import + + enum link__Purpose { + SECURITY + EXECUTION + } + scalar join__FieldSet + enum join__Graph { USER @join__graph(name: "user", url: "http://localhost:4001/graphql") ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") } - type Query { + type Query @join__type(graph: USER) @join__type(graph: ORGA){ test(input: InputEnum): String @join__field(graph: USER) } - enum InputEnum { + enum InputEnum @join__type(graph: USER) @join__type(graph: ORGA) { A B }"#; diff --git a/apollo-router/src/spec/query/tests.rs b/apollo-router/src/spec/query/tests.rs index 11f078af5d..bb13004402 100644 --- a/apollo-router/src/spec/query/tests.rs +++ b/apollo-router/src/spec/query/tests.rs @@ -118,8 +118,8 @@ impl FormatTest { let query_type_name = self.query_type_name.unwrap_or("Query"); let schema = match self.federation_version { - FederationVersion::Fed1 => with_supergraph_boilerplate(schema, query_type_name), - FederationVersion::Fed2 => with_supergraph_boilerplate_fed2(schema, query_type_name), + FederationVersion::Fed1 => with_supergraph_boilerplate_fed1(schema, query_type_name), + FederationVersion::Fed2 => with_supergraph_boilerplate(schema, query_type_name), }; let schema = Schema::parse(&schema, &Default::default()).expect("could not parse schema"); @@ -161,7 +161,7 @@ impl FormatTest { } } -fn with_supergraph_boilerplate(content: &str, query_type_name: &str) -> String { +fn with_supergraph_boilerplate_fed1(content: &str, query_type_name: &str) -> String { format!( r#" schema @@ -173,7 +173,7 @@ fn with_supergraph_boilerplate(content: &str, query_type_name: &str) -> String { }} directive @core(feature: String!) repeatable on SCHEMA directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + directive @inaccessible on OBJECT| FIELD_DEFINITION | INTERFACE | UNION enum join__Graph {{ TEST @join__graph(name: "test", url: "http://localhost:4001/graphql") }} @@ -183,36 +183,28 @@ fn with_supergraph_boilerplate(content: &str, query_type_name: &str) -> String { ) } -fn with_supergraph_boilerplate_fed2(content: &str, query_type_name: &str) -> String { +fn with_supergraph_boilerplate(content: &str, query_type_name: &str) -> String { format!( r#" schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.2", for: EXECUTION) @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.2", for: EXECUTION) {{ query: {query_type_name} }} directive @join__field(graph: join__Graph!, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA - directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ENUM | ENUM_VALUE | SCALAR | INPUT_OBJECT | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION + directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION scalar join__FieldSet 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 + SECURITY + EXECUTION }} enum join__Graph {{ @@ -1788,16 +1780,24 @@ fn variable_validation() { let schema = r#" schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + { query: Query mutation: Mutation } - directive @core(feature: String!) repeatable on SCHEMA + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + 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 + + scalar join__FieldSet + scalar link__Import + + enum link__Purpose { + SECURITY + EXECUTION + } + enum join__Graph { TEST @join__graph(name: "test", url: "http://localhost:4001/graphql") } @@ -1805,7 +1805,7 @@ fn variable_validation() { type Mutation{ foo(input: FooInput!): FooResponse! } - type Query{ + type Query @join__type(graph: TEST){ data: String } @@ -5074,37 +5074,26 @@ fn fragment_on_interface_on_query() { let schema = r#"schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.2", for: EXECUTION) - @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) { query: MyQueryObject } - directive @join__field(graph: join__Graph!, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA - directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ENUM | ENUM_VALUE | SCALAR | INPUT_OBJECT | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION scalar join__FieldSet 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 + SECURITY + EXECUTION } enum join__Graph { TEST @join__graph(name: "test", url: "http://localhost:4001/graphql") } - type MyQueryObject implements Interface { + type MyQueryObject implements Interface @join__type(graph: TEST){ object: MyObject other: String } @@ -5690,20 +5679,35 @@ fn test_query_not_named_query() { let schema = Schema::parse( r#" schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + { query: TheOneAndOnlyQuery } - directive @core(feature: String!) repeatable on SCHEMA + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + 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__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + + scalar join__FieldSet + 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 + } + enum join__Graph { TEST @join__graph(name: "test", url: "http://localhost:4001/graphql") } - type TheOneAndOnlyQuery { example: Boolean } + type TheOneAndOnlyQuery @join__type(graph: TEST) { example: Boolean } "#, &Default::default(), ) @@ -5729,20 +5733,27 @@ fn filtered_defer_fragment() { let schema = Schema::parse( r#" schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + { + query: Query } - directive @core(feature: String!) repeatable on SCHEMA + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + 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 + + scalar join__FieldSet + scalar link__Import + + enum link__Purpose { + SECURITY + EXECUTION + } enum join__Graph { TEST @join__graph(name: "test", url: "http://localhost:4001/graphql") } - type Query { + type Query @join__type(graph: TEST) { a: A } diff --git a/apollo-router/src/spec/schema.rs b/apollo-router/src/spec/schema.rs index 05546910ca..edb6c3bbac 100644 --- a/apollo-router/src/spec/schema.rs +++ b/apollo-router/src/spec/schema.rs @@ -361,12 +361,23 @@ mod tests { "{}\n{}", r#" schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") { + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + { query: Query } - directive @core(feature: String!) repeatable on SCHEMA + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + 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__graph(name: String!, url: String!) on ENUM_VALUE + + scalar link__Import + scalar join__FieldSet + + enum link__Purpose { + SECURITY + EXECUTION + } + enum join__Graph { TEST @join__graph(name: "test", url: "http://localhost:4001/graphql") } @@ -457,27 +468,7 @@ mod tests { #[test] fn routing_urls() { - let schema = r#" - schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") - { - query: Query - } - type Query { - me: String - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - - enum join__Graph { - ACCOUNTS @join__graph(name:"accounts" url: "http://localhost:4001/graphql") - INVENTORY - @join__graph(name: "inventory", url: "http://localhost:4004/graphql") - PRODUCTS - @join__graph(name: "products" url: "http://localhost:4003/graphql") - REVIEWS @join__graph(name: "reviews" url: "http://localhost:4002/graphql") - }"#; + let schema = include_str!("../testdata/minimal_local_inventory_supergraph.graphql"); let schema = Schema::parse(schema, &Default::default()).unwrap(); assert_eq!(schema.subgraphs.len(), 4); @@ -497,7 +488,7 @@ mod tests { .get("inventory") .map(|s| s.to_string()) .as_deref(), - Some("http://localhost:4004/graphql"), + Some("http://localhost:4002/graphql"), "Incorrect url for inventory" ); @@ -517,7 +508,7 @@ mod tests { .get("reviews") .map(|s| s.to_string()) .as_deref(), - Some("http://localhost:4002/graphql"), + Some("http://localhost:4004/graphql"), "Incorrect url for reviews" ); @@ -543,7 +534,7 @@ mod tests { fn federation_version() { // @core directive let schema = Schema::parse( - include_str!("../testdata/minimal_supergraph.graphql"), + include_str!("../testdata/minimal_fed1_supergraph.graphql"), &Default::default(), ) .unwrap(); @@ -551,7 +542,7 @@ mod tests { // @link directive let schema = Schema::parse( - include_str!("../testdata/minimal_fed2_supergraph.graphql"), + include_str!("../testdata/minimal_supergraph.graphql"), &Default::default(), ) .unwrap(); @@ -567,7 +558,7 @@ mod tests { assert_eq!( Schema::schema_id(&schema.raw_sdl), - "8e2021d131b23684671c3b85f82dfca836908c6a541bbd5c3772c66e7f8429d8".to_string() + "23bcf0ea13a4e0429c942bba59573ba70b8d6970d73ad00c5230d08788bb1ba2".to_string() ); } } diff --git a/apollo-router/src/testdata/a_b_supergraph.graphql b/apollo-router/src/testdata/a_b_supergraph.graphql new file mode 100644 index 0000000000..d51619dfa0 --- /dev/null +++ b/apollo-router/src/testdata/a_b_supergraph.graphql @@ -0,0 +1,60 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query + mutation: Mutation +} + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar join__FieldSet +scalar link__Import + +enum join__Graph { + A @join__graph(name: "A", url: "http://localhost:4001") + B @join__graph(name: "B", url: "http://localhost:4004") +} + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation @join__type(graph: A) @join__type(graph: B) { + mutationA: Mutation @join__field(graph: A) + mutationB: Boolean @join__field(graph: B) +} + +type Query @join__type(graph: A) @join__type(graph: B) { + query: Boolean @join__field(graph: A) +} diff --git a/apollo-router/src/testdata/contract_schema.graphql b/apollo-router/src/testdata/contract_schema.graphql index 74db321db8..12ee987bf6 100644 --- a/apollo-router/src/testdata/contract_schema.graphql +++ b/apollo-router/src/testdata/contract_schema.graphql @@ -1,40 +1,119 @@ -schema @core(feature: "https://specs.apollo.dev/core/v0.1") @core(feature: "https://specs.apollo.dev/join/v0.1") @core(feature: "https://specs.apollo.dev/tag/v0.1") @apollo_studio_metadata(launchId: "2396d4fb-a1e4-457d-8da4-347479b852f1", buildId: "2396d4fb-a1e4-457d-8da4-347479b852f1", checkId: null) @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") { +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @apollo_studio_metadata( + launchId: "2396d4fb-a1e4-457d-8da4-347479b852f1" + buildId: "2396d4fb-a1e4-457d-8da4-347479b852f1" + checkId: null + ) { query: Query mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION +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 @apollo_studio_metadata(launchId: String, buildId: String, checkId: String) on SCHEMA +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + directive @tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT + | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE + | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA scalar join__FieldSet +directive @apollo_studio_metadata( + launchId: String + buildId: String + checkId: String +) on SCHEMA + enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "http://accounts.demo.starstuff.dev/graphql") - INVENTORY @join__graph(name: "inventory", url: "http://inventory.demo.starstuff.dev/graphql") - PRODUCTS @join__graph(name: "products", url: "http://products.demo.starstuff.dev/graphql") - REVIEWS @join__graph(name: "reviews", url: "http://reviews.demo.starstuff.dev/graphql") + ACCOUNTS + @join__graph( + name: "accounts" + url: "http://accounts.demo.starstuff.dev/graphql" + ) + INVENTORY + @join__graph( + name: "inventory" + url: "http://inventory.demo.starstuff.dev/graphql" + ) + PRODUCTS + @join__graph( + name: "products" + url: "http://products.demo.starstuff.dev/graphql" + ) + REVIEWS + @join__graph( + name: "reviews" + url: "http://reviews.demo.starstuff.dev/graphql" + ) +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION } type Mutation { createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) } -type Product @join__owner(graph: PRODUCTS) @join__type(graph: PRODUCTS, key: "upc") @join__type(graph: REVIEWS, key: "upc") @join__type(graph: INVENTORY, key: "upc") { - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible +type Product + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") + @join__type(graph: INVENTORY, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) @@ -49,14 +128,16 @@ type Query { topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review @join__owner(graph: REVIEWS) @join__type(graph: REVIEWS, key: "id") { +type Review @join__type(graph: REVIEWS, key: "id") { author: User @join__field(graph: REVIEWS, provides: "username") body: String @join__field(graph: REVIEWS) id: ID! @join__field(graph: REVIEWS) product: Product @join__field(graph: REVIEWS) } -type User @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") @join__type(graph: REVIEWS, key: "id") { +type User + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { id: ID! @join__field(graph: ACCOUNTS) name: String @join__field(graph: ACCOUNTS) reviews: [Review] @join__field(graph: REVIEWS) diff --git a/apollo-router/src/testdata/inaccessible_on_non_core.graphql b/apollo-router/src/testdata/inaccessible_on_non_core.graphql index 3f8c8bcb24..a43a836c35 100644 --- a/apollo-router/src/testdata/inaccessible_on_non_core.graphql +++ b/apollo-router/src/testdata/inaccessible_on_non_core.graphql @@ -1,26 +1,60 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2") - @core(feature: "https://specs.apollo.dev/join/v0.2", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) - -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) { query: Query } -directive @core(feature: String!, as: String, for: core__Purpose) repeatable on SCHEMA +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) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE +directive @join__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 -directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR +scalar join__FieldSet directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION -enum core__Purpose { +enum join__Graph { + SUBGRAPH @join__graph(name: "users", url: "http://localhost:4001") +} + +scalar link__Import + +enum link__Purpose { """ `SECURITY` features provide metadata necessary to securely resolve fields. """ @@ -32,13 +66,6 @@ enum core__Purpose { EXECUTION } -scalar join__FieldSet - -enum join__Graph { - SUBGRAPH @join__graph(name: "users", url: "http://localhost:4001") -} - - input InputObject { someField: String privateField: String @inaccessible diff --git a/apollo-router/src/testdata/invalid_minimal_supergraph_missing_subgraph_url.graphql b/apollo-router/src/testdata/invalid_minimal_supergraph_missing_subgraph_url.graphql new file mode 100644 index 0000000000..fd4178d111 --- /dev/null +++ b/apollo-router/src/testdata/invalid_minimal_supergraph_missing_subgraph_url.graphql @@ -0,0 +1,31 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query +} + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA +scalar link__Import + +enum join__Graph { + ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001/graphql") + INVENTORY + @join__graph(name: "inventory", url: "http://localhost:4002/graphql") + PRODUCTS @join__graph(name: "products", url: "http://localhost:4003/graphql") + REVIEWS @join__graph(name: "reviews", url: "") +} + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Query { + me: String +} diff --git a/apollo-router/src/testdata/invalid_supergraph.graphql b/apollo-router/src/testdata/invalid_supergraph.graphql index 38b1048051..e2a51efb7e 100644 --- a/apollo-router/src/testdata/invalid_supergraph.graphql +++ b/apollo-router/src/testdata/invalid_supergraph.graphql @@ -1,65 +1,118 @@ # This supergraph is almost valid. the only difference is that the reviews field in the Product type joins on PRODUCTS which is incorrect -schema @core(feature: "https://specs.apollo.dev/core/v0.1") @core(feature: "https://specs.apollo.dev/join/v0.1") @core(feature: "https://specs.apollo.dev/tag/v0.1") @apollo_studio_metadata(launchId: "2396d4fb-a1e4-457d-8da4-347479b852f1", buildId: "2396d4fb-a1e4-457d-8da4-347479b852f1", checkId: null) @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") { +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/tag/v0.3") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA - -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION - -directive @apollo_studio_metadata(launchId: String, buildId: String, checkId: String) on SCHEMA - -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - +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 @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION + +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +scalar link__Import scalar join__FieldSet enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001/graphql") - INVENTORY @join__graph(name: "inventory", url: "http://localhost:4004/graphql") - PRODUCTS @join__graph(name: "products", url: "http://localhost:4003/graphql") - REVIEWS @join__graph(name: "reviews", url: "http://localhost:4002/graphql") + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +enum link__Purpose { + SECURITY + EXECUTION } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } -type Product @join__owner(graph: PRODUCTS) @join__type(graph: PRODUCTS, key: "upc") @join__type(graph: REVIEWS, key: "upc") @join__type(graph: INVENTORY, key: "upc") { - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible +type Product + @join__type(graph: ACCOUNTS, key: "upc", extension: true) + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) price: Int @join__field(graph: PRODUCTS) - reviews: [Review] @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: PRODUCTS) # This is deliberately incorrect reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") upc: String! @join__field(graph: PRODUCTS) weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review @join__owner(graph: REVIEWS) @join__type(graph: REVIEWS, key: "id") { +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! author: User @join__field(graph: REVIEWS, provides: "username") body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) product: Product @join__field(graph: REVIEWS) } -type User @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") @join__type(graph: REVIEWS, key: "id") { - id: ID! @join__field(graph: ACCOUNTS) +type User + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) reviews: [Review] @join__field(graph: REVIEWS) username: String @join__field(graph: ACCOUNTS) diff --git a/apollo-router/src/testdata/minimal_fed1_supergraph.graphql b/apollo-router/src/testdata/minimal_fed1_supergraph.graphql new file mode 100644 index 0000000000..7df337ea43 --- /dev/null +++ b/apollo-router/src/testdata/minimal_fed1_supergraph.graphql @@ -0,0 +1,32 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.apollo.dev/join/v0.1") { + query: Query +} + +directive @core(feature: String!) repeatable on SCHEMA + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet +) on FIELD_DEFINITION + +directive @join__type( + graph: join__Graph! + key: join__FieldSet +) repeatable on OBJECT | INTERFACE + +directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +scalar join__FieldSet + +enum join__Graph { + ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001/graphql") +} + +type Query { + me: String +} diff --git a/apollo-router/src/testdata/minimal_fed2_supergraph.graphql b/apollo-router/src/testdata/minimal_fed2_supergraph.graphql deleted file mode 100644 index 896c6f95e4..0000000000 --- a/apollo-router/src/testdata/minimal_fed2_supergraph.graphql +++ /dev/null @@ -1,44 +0,0 @@ -schema - @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) -{ - query: Query -} - -directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE - -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION - -directive @join__graph(name: String!, url: String!) on ENUM_VALUE - -directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE - -directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR - -directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION - -directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA - -scalar join__FieldSet - -enum join__Graph { - SUBGRAPH_A @join__graph(name: "subgraph-a", url: "http://graphql.subgraph-a.svc.cluster.local:4000") -} - -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: SUBGRAPH_A) { - me: String @join__field(graph: SUBGRAPH_A) -} diff --git a/apollo-router/src/testdata/minimal_local_inventory_supergraph.graphql b/apollo-router/src/testdata/minimal_local_inventory_supergraph.graphql new file mode 100644 index 0000000000..9cf20a6f0d --- /dev/null +++ b/apollo-router/src/testdata/minimal_local_inventory_supergraph.graphql @@ -0,0 +1,36 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query +} + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA +scalar link__Import + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +enum join__Graph { + ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001/graphql") + INVENTORY + @join__graph(name: "inventory", url: "http://localhost:4002/graphql") + PRODUCTS @join__graph(name: "products", url: "http://localhost:4003/graphql") + REVIEWS @join__graph(name: "reviews", url: "http://localhost:4004/graphql") +} + +enum link__Purpose { + SECURITY + EXECUTION +} +type Query { + me: String +} diff --git a/apollo-router/src/testdata/minimal_supergraph.graphql b/apollo-router/src/testdata/minimal_supergraph.graphql index 7df337ea43..ac0b1860af 100644 --- a/apollo-router/src/testdata/minimal_supergraph.graphql +++ b/apollo-router/src/testdata/minimal_supergraph.graphql @@ -1,32 +1,57 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") { + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query } -directive @core(feature: String!) repeatable on SCHEMA - directive @join__field( graph: join__Graph requires: join__FieldSet provides: join__FieldSet -) on FIELD_DEFINITION + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE directive @join__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 @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +directive @join__implements( + graph: join__Graph! + interface: String! ) repeatable on OBJECT | INTERFACE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE - -directive @join__graph(name: String!, url: String!) on ENUM_VALUE - scalar join__FieldSet +scalar link__Import enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001/graphql") + SUBGRAPH_A + @join__graph( + name: "subgraph-a" + url: "http://graphql.subgraph-a.svc.cluster.local:4000" + ) +} + +enum link__Purpose { + SECURITY + EXECUTION } -type Query { - me: String +type Query @join__type(graph: SUBGRAPH_A) { + me: String @join__field(graph: SUBGRAPH_A) } diff --git a/apollo-router/src/testdata/orga_supergraph.graphql b/apollo-router/src/testdata/orga_supergraph.graphql new file mode 100644 index 0000000000..a6ddb679d0 --- /dev/null +++ b/apollo-router/src/testdata/orga_supergraph.graphql @@ -0,0 +1,79 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query + subscription: Subscription +} + +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar join__FieldSet +scalar link__Import + +enum join__Graph { + USER @join__graph(name: "user", url: "http://localhost:4001/graphql") + ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") +} + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Query @join__type(graph: USER) @join__type(graph: ORGA) { + currentUser: User @join__field(graph: USER) + otherUser: User @join__field(graph: USER) + orga(id: ID): Organization @join__field(graph: ORGA) +} + +type Subscription @join__type(graph: USER) { + userWasCreated: User +} + +type User + @join__type(graph: ORGA, key: "id") + @join__type(graph: USER, key: "id") { + id: ID! + name: String @join__field(graph: USER) + phone: String @join__field(graph: USER) + activeOrganization: Organization @join__field(graph: USER) + allOrganizations: [Organization] @join__field(graph: USER) +} + +type Organization + @join__type(graph: ORGA, key: "id") + @join__type(graph: USER, key: "id") { + id: ID + creatorUser: User @join__field(graph: ORGA) + name: String @join__field(graph: ORGA) + nonNullId: ID! @join__field(graph: ORGA) + suborga: [Organization] @join__field(graph: ORGA) +} diff --git a/apollo-router/src/testdata/starstuff@current.graphql b/apollo-router/src/testdata/starstuff@current.graphql index abe3698edf..ff57927866 100644 --- a/apollo-router/src/testdata/starstuff@current.graphql +++ b/apollo-router/src/testdata/starstuff@current.graphql @@ -1,62 +1,139 @@ -schema @core(feature: "https://specs.apollo.dev/core/v0.1") @core(feature: "https://specs.apollo.dev/join/v0.1") @core(feature: "https://specs.apollo.dev/tag/v0.1") @apollo_studio_metadata(launchId: "2396d4fb-a1e4-457d-8da4-347479b852f1", buildId: "2396d4fb-a1e4-457d-8da4-347479b852f1", checkId: null) { +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @apollo_studio_metadata( + launchId: "2396d4fb-a1e4-457d-8da4-347479b852f1" + buildId: "2396d4fb-a1e4-457d-8da4-347479b852f1" + checkId: null + ) { query: Query mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +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 @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "http://accounts.demo.starstuff.dev/graphql") - INVENTORY @join__graph(name: "inventory", url: "http://inventory.demo.starstuff.dev/graphql") - PRODUCTS @join__graph(name: "products", url: "http://products.demo.starstuff.dev/graphql") - REVIEWS @join__graph(name: "reviews", url: "http://reviews.demo.starstuff.dev/graphql") + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +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 Product @join__owner(graph: PRODUCTS) @join__type(graph: PRODUCTS, key: "upc") @join__type(graph: REVIEWS, key: "upc") @join__type(graph: INVENTORY, key: "upc") { - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) +} + +type Product + @join__type(graph: ACCOUNTS, key: "upc", extension: true) + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) + recommendedProducts: [Product] @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review @join__owner(graph: REVIEWS) @join__type(graph: REVIEWS, key: "id") { +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + product: Product } -type User @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") @join__type(graph: REVIEWS, key: "id") { - id: ID! @join__field(graph: ACCOUNTS) +type User + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } -directive @apollo_studio_metadata(launchId: String, buildId: String, checkId: String) on SCHEMA +directive @apollo_studio_metadata( + launchId: String + buildId: String + checkId: String +) on SCHEMA diff --git a/apollo-router/src/testdata/supergraph.graphql b/apollo-router/src/testdata/supergraph.graphql index 9a64dd4358..c23dd320ca 100644 --- a/apollo-router/src/testdata/supergraph.graphql +++ b/apollo-router/src/testdata/supergraph.graphql @@ -1,66 +1,111 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "https://accounts.demo.starstuff.dev/") - INVENTORY @join__graph(name: "inventory" url: "https://inventory.demo.starstuff.dev/") - PRODUCTS @join__graph(name: "products" url: "https://products.demo.starstuff.dev/") - REVIEWS @join__graph(name: "reviews" url: "https://reviews.demo.starstuff.dev/") + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +scalar link__Import + +enum link__Purpose { + SECURITY + EXECUTION } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - upc: String! @join__field(graph: PRODUCTS) - name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + name: String @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: REVIEWS) - body: String @join__field(graph: REVIEWS) +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") product: Product @join__field(graph: REVIEWS) } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) - username: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) } diff --git a/apollo-router/src/testdata/supergraph_missing_name.graphql b/apollo-router/src/testdata/supergraph_missing_name.graphql index 6b21b4f4e7..91d122492e 100644 --- a/apollo-router/src/testdata/supergraph_missing_name.graphql +++ b/apollo-router/src/testdata/supergraph_missing_name.graphql @@ -1,65 +1,127 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query + mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "https://accounts.demo.starstuff.dev/") - INVENTORY @join__graph(name: "inventory" url: "https://inventory.demo.starstuff.dev/") - PRODUCTS @join__graph(name: "products" url: "https://products.demo.starstuff.dev/") - REVIEWS @join__graph(name: "reviews" url: "https://reviews.demo.starstuff.dev/") + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - upc: String! @join__field(graph: PRODUCTS) - name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + name: String @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) + reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) + recommendedProducts: [Product] @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: REVIEWS) - body: String @join__field(graph: REVIEWS) +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") - product: Product @join__field(graph: REVIEWS) + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) - username: String @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) } diff --git a/apollo-router/src/uplink/testdata/oss.graphql b/apollo-router/src/uplink/testdata/oss.graphql index 7df337ea43..d7c87efa71 100644 --- a/apollo-router/src/uplink/testdata/oss.graphql +++ b/apollo-router/src/uplink/testdata/oss.graphql @@ -1,30 +1,70 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") { + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE directive @join__field( graph: join__Graph requires: join__FieldSet provides: join__FieldSet -) on FIELD_DEFINITION + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE directive @join__type( graph: join__Graph! key: join__FieldSet -) repeatable on OBJECT | INTERFACE + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet enum join__Graph { ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001/graphql") + INVENTORY + @join__graph(name: "inventory", url: "http://localhost:4002/graphql") + PRODUCTS @join__graph(name: "products", url: "http://localhost:4003/graphql") + REVIEWS @join__graph(name: "reviews", url: "http://localhost:4004/graphql") +} + +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 { diff --git a/apollo-router/testing_schema.graphql b/apollo-router/testing_schema.graphql index f724f9ce2b..ffe2c58c14 100644 --- a/apollo-router/testing_schema.graphql +++ b/apollo-router/testing_schema.graphql @@ -1,79 +1,118 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation subscription: Subscription } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet +scalar link__Import enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "http://localhost:4001") - INVENTORY @join__graph(name: "inventory" url: "http://localhost:4004") - PRODUCTS @join__graph(name: "products" url: "http://localhost:4003") - REVIEWS @join__graph(name: "reviews" url: "http://localhost:4002") + ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001") + INVENTORY @join__graph(name: "inventory", url: "http://localhost:4004") + PRODUCTS @join__graph(name: "products", url: "http://localhost:4003") + REVIEWS @join__graph(name: "reviews", url: "http://localhost:4002") +} + +enum link__Purpose { + SECURITY + EXECUTION } -type Mutation { +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) } -type Subscription { +type Subscription @join__type(graph: ACCOUNTS) @join__type(graph: REVIEWS) { userWasCreated: User @join__field(graph: ACCOUNTS) reviewAdded: Review @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ - author: User @join__field(graph: REVIEWS, provides: "username") +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) product: Product @join__field(graph: REVIEWS) + author: User @join__field(graph: REVIEWS, provides: "username") } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/fixtures/accounts.graphql b/apollo-router/tests/fixtures/accounts.graphql index 6cb47c0d5c..33468356a1 100644 --- a/apollo-router/tests/fixtures/accounts.graphql +++ b/apollo-router/tests/fixtures/accounts.graphql @@ -1,9 +1,20 @@ -extend type Query { +extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@shareable", "@external"] + ) + +type Query { me: User + recommendedProducts: [Product] } type User @key(fields: "id") { id: ID! name: String - username: String + username: String @shareable +} + +extend type Product @key(fields: "upc") { + upc: String! @external } diff --git a/apollo-router/tests/fixtures/inventory.graphql b/apollo-router/tests/fixtures/inventory.graphql index 5db69e4267..b9a58c0fc5 100644 --- a/apollo-router/tests/fixtures/inventory.graphql +++ b/apollo-router/tests/fixtures/inventory.graphql @@ -1,13 +1,13 @@ -directive @tag(name: String!) repeatable on - | FIELD_DEFINITION - | INTERFACE - | OBJECT - | UNION +extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@external", "@requires"] + ) -extend type Product @key(fields: "upc") { - upc: String! @external +type Product @key(fields: "upc") { + upc: String! weight: Int @external price: Int @external - inStock: Boolean @tag(name: "private") + inStock: Boolean shippingEstimate: Int @requires(fields: "price weight") } diff --git a/apollo-router/tests/fixtures/products.graphql b/apollo-router/tests/fixtures/products.graphql index 0b575c1f9b..8cd5747f1d 100644 --- a/apollo-router/tests/fixtures/products.graphql +++ b/apollo-router/tests/fixtures/products.graphql @@ -1,4 +1,11 @@ -extend type Mutation { +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + +type Query { + topProducts(first: Int = 5): [Product] +} + +type Mutation { createProduct(upc: ID!, name: String): Product } @@ -8,7 +15,3 @@ type Product @key(fields: "upc") { price: Int weight: Int } - -extend type Query { - topProducts(first: Int = 5): [Product] -} diff --git a/apollo-router/tests/fixtures/reviews.graphql b/apollo-router/tests/fixtures/reviews.graphql index e42ed6fbca..46254285c4 100644 --- a/apollo-router/tests/fixtures/reviews.graphql +++ b/apollo-router/tests/fixtures/reviews.graphql @@ -1,11 +1,11 @@ -extend type Mutation { - createReview(upc: ID!, id: ID!, body: String): Review -} +extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@external", "@provides"] + ) -extend type Product @key(fields: "upc") { - upc: String! @external - reviews: [Review] - reviewsForAuthor(authorID: ID!): [Review] +type Mutation { + createReview(upc: ID!, id: ID!, body: String): Review } type Review @key(fields: "id") { @@ -15,8 +15,14 @@ type Review @key(fields: "id") { product: Product } -extend type User @key(fields: "id") { - id: ID! @external +type User @key(fields: "id") { + id: ID! username: String @external reviews: [Review] } + +type Product @key(fields: "upc") { + upc: String! + reviews: [Review] + reviewsForAuthor(authorID: ID!): [Review] +} diff --git a/apollo-router/tests/fixtures/supergraph.graphql b/apollo-router/tests/fixtures/supergraph.graphql index 971174056a..47036a804c 100644 --- a/apollo-router/tests/fixtures/supergraph.graphql +++ b/apollo-router/tests/fixtures/supergraph.graphql @@ -1,90 +1,131 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA - -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION +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 -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION +scalar join__FieldSet -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +scalar link__Import +enum link__Purpose { """ `SECURITY` features provide metadata necessary to securely resolve fields. """ SECURITY -} -scalar join__FieldSet - -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + inStock: Boolean @join__field(graph: INVENTORY) @inaccessible + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) + recommendedProducts: [Product] @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/integration/query_planner.rs b/apollo-router/tests/integration/query_planner.rs index 9c85c99690..88ace09147 100644 --- a/apollo-router/tests/integration/query_planner.rs +++ b/apollo-router/tests/integration/query_planner.rs @@ -13,7 +13,7 @@ const BOTH_BEST_EFFORT_QP: &str = "experimental_query_planner_mode: both_best_ef async fn fed1_schema_with_legacy_qp() { let mut router = IntegrationTest::builder() .config(LEGACY_QP) - .supergraph("../examples/graphql/local.graphql") + .supergraph("../examples/graphql/supergraph-fed1.graphql") .build() .await; router.start().await; @@ -26,7 +26,7 @@ async fn fed1_schema_with_legacy_qp() { async fn fed1_schema_with_new_qp() { let mut router = IntegrationTest::builder() .config(NEW_QP) - .supergraph("../examples/graphql/local.graphql") + .supergraph("../examples/graphql/supergraph-fed1.graphql") .build() .await; router.start().await; @@ -44,7 +44,7 @@ async fn fed1_schema_with_new_qp() { async fn fed1_schema_with_both_qp() { let mut router = IntegrationTest::builder() .config(BOTH_QP) - .supergraph("../examples/graphql/local.graphql") + .supergraph("../examples/graphql/supergraph-fed1.graphql") .build() .await; router.start().await; @@ -62,7 +62,7 @@ async fn fed1_schema_with_both_qp() { async fn fed1_schema_with_both_best_effort_qp() { let mut router = IntegrationTest::builder() .config(BOTH_BEST_EFFORT_QP) - .supergraph("../examples/graphql/local.graphql") + .supergraph("../examples/graphql/supergraph-fed1.graphql") .build() .await; router.start().await; @@ -84,7 +84,7 @@ async fn fed1_schema_with_legacy_qp_reload_to_new_keep_previous_config() { let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); let mut router = IntegrationTest::builder() .config(config) - .supergraph("../examples/graphql/local.graphql") + .supergraph("../examples/graphql/supergraph-fed1.graphql") .build() .await; router.start().await; @@ -111,7 +111,7 @@ async fn fed1_schema_with_legacy_qp_reload_to_both_best_effort_keep_previous_con let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); let mut router = IntegrationTest::builder() .config(config) - .supergraph("../examples/graphql/local.graphql") + .supergraph("../examples/graphql/supergraph-fed1.graphql") .build() .await; router.start().await; @@ -143,7 +143,7 @@ async fn fed2_schema_with_new_qp() { let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{NEW_QP}"); let mut router = IntegrationTest::builder() .config(config) - .supergraph("../examples/graphql/supergraph-fed2.graphql") + .supergraph("../examples/graphql/supergraph.graphql") .build() .await; router.start().await; diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index 16724575aa..30b71d46b3 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -1,3 +1,28 @@ +// The redis cache keys in this file have to change whenever the following change: +// * the supergraph schema +// * experimental_query_planner_mode +// * federation version +// +// How to get the new cache key: +// If you have redis running locally, you can skip step 1 and proceed with steps 2-3. +// 1. run `docker-compose up -d` and connect to the redis container by running `docker-compose exec redis /bin/bash`. +// 2. Run the `redis-cli` command from the shell and start the redis `monitor` command. +// 3. Run the failing test and yank the updated cache key from the redis logs. It will be the key following `SET`, for example: +// ```bash +// 1724831727.472732 [0 127.0.0.1:56720] "SET" +// "plan:0:v2.8.5:70f115ebba5991355c17f4f56ba25bb093c519c4db49a30f3b10de279a4e3fa4:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:4f9f0183101b2f249a364b98adadfda6e5e2001d1f2465c988428cf1ac0b545f" +// "{\"Ok\":{\"Plan\":{\"plan\":{\"usage_reporting\":{\"statsReportKey\":\"# +// -\\n{topProducts{name +// name}}\",\"referencedFieldsByType\":{\"Product\":{\"fieldNames\":[\"name\"],\"isInterface\":false},\"Query\":{\"fieldNames\":[\"topProducts\"],\"isInterface\":false}}},\"root\":{\"kind\":\"Fetch\",\"serviceName\":\"products\",\"variableUsages\":[],\"operation\":\"{topProducts{name +// name2:name}}\",\"operationName\":null,\"operationKind\":\"query\",\"id\":null,\"inputRewrites\":null,\"outputRewrites\":null,\"contextRewrites\":null,\"schemaAwareHash\":\"121b9859eba2d8fa6dde0a54b6e3781274cf69f7ffb0af912e92c01c6bfff6ca\",\"authorization\":{\"is_authenticated\":false,\"scopes\":[],\"policies\":[]}},\"formatted_query_plan\":\"QueryPlan +// {\\n Fetch(service: \\\"products\\\") {\\n {\\n topProducts {\\n +// name\\n name2: name\\n }\\n }\\n +// n },\\n}\",\"query\":{\"string\":\"{\\n topProducts {\\n name\\n +// name2: name\\n +// }\\n}\\n\",\"fragments\":{\"map\":{}},\"operations\":[{\"name\":null,\"kind\":\"query\",\"type_name\":\"Query\",\"selection_set\":[{\"Field\":{\"name\":\"topProducts\",\"alias\":null,\"selection_set\":[{\"Field\":{\"name\":\"name\",\"alias\":null,\"selection_set\":null,\"field_type\":{\"Named\":\"String\"},\"include_skip\":{\"include\":\"Yes\",\"skip\":\"No\"}}},{\"Field\":{\"name\":\"name\",\"alias\":\"name2\",\"selection_set\":null,\"field_type\":{\"Named\":\"String\"},\"include_skip\":{\"include\":\"Yes\",\"skip\":\"No\"}}}],\"field_type\":{\"List\":{\"Named\":\"Product\"}},\"include_skip\":{\"include\":\"Yes\",\"skip\":\"No\"}}}],\"variables\":{}}],\"subselections\":{},\"unauthorized\":{\"paths\":[],\"errors\":{\"log\":true,\"response\":\"errors\"}},\"filtered_query\":null,\"defer_stats\":{\"has_defer\":false,\"has_unconditional_defer\":false,\"conditional_defer_variable_names\":[]},\"is_original\":true,\"schema_aware_hash\":[20,152,93,92,189,0,240,140,9,65,84,255,4,76,202,231,69,183,58,121,37,240,0,109,198,125,1,82,12,42,179,189]},\"query_metrics\":{\"depth\":2,\"height\":3,\"root_fields\":1,\"aliases\":1},\"estimated_size\":0}}}}" +// "EX" "10" +// ``` + use apollo_router::plugin::test::MockSubgraph; use apollo_router::services::router; use apollo_router::services::supergraph; @@ -22,11 +47,8 @@ use crate::integration::IntegrationTest; #[tokio::test(flavor = "multi_thread")] async fn query_planner_cache() -> Result<(), BoxError> { // If this test fails and the cache key format changed you'll need to update the key here. - // 1. Force this test to run locally by removing the cfg() line at the top of this file. - // 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.9.0:16385ebef77959fcdc520ad507eb1f7f7df28f1d54a0569e3adabcb4cd00d7ce:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:8ecc6cbc98bab2769e6666a72ba47a4ebd90e6f62256ddcbdc7f352a805e0fe6"; + // Look at the top of the file for instructions on getting the new cache key. + let known_cache_key = "plan:0:v2.9.0:70f115ebba5991355c17f4f56ba25bb093c519c4db49a30f3b10de279a4e3fa4:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:4f9f0183101b2f249a364b98adadfda6e5e2001d1f2465c988428cf1ac0b545f"; let config = RedisConfig::from_url("redis://127.0.0.1:6379").unwrap(); let client = RedisClient::new(config, None, None, None); @@ -922,13 +944,13 @@ async fn connection_failure_blocks_startup() { async fn query_planner_redis_update_query_fragments() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_query_fragments.router.yaml"), - "plan:0:v2.9.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:cda2b4e476fdce9c4c435627b26cedd177cfbe04ab335fc3e3d895c0d79d965e", + "plan:0:v2.9.0:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:a52c81e3e2e47c8363fbcd2653e196431c15716acc51fce4f58d9368ac4c2d8d", ) .await; } #[tokio::test(flavor = "multi_thread")] -#[ignore = "extraction of subgraphs from supergraph is not yet implemented"] +#[ignore = "the cache key for different query planner modes is currently different"] async fn query_planner_redis_update_planner_mode() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_query_planner_mode.router.yaml"), @@ -939,40 +961,84 @@ async fn query_planner_redis_update_planner_mode() { #[tokio::test(flavor = "multi_thread")] async fn query_planner_redis_update_introspection() { + // If this test fails and the cache key format changed you'll need to update + // the key here. Look at the top of the file for instructions on getting + // the new cache key. + // + // You first need to follow the process and update the key in + // `test_redis_query_plan_config_update`, and then update the key in this + // test. + // + // This test requires graphos license, so make sure you have + // "TEST_APOLLO_KEY" and "TEST_APOLLO_GRAPH_REF" env vars set, otherwise the + // test just passes locally. test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_introspection.router.yaml"), - "plan:0:v2.9.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:259dd917e4de09b5469629849b91e8ffdfbed2587041fad68b5963369bb13283", + "plan:0:v2.9.0:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:99a70d6c967eea3bc68721e1094f586f5ae53c7e12f83a650abd5758c372d048", ) .await; } #[tokio::test(flavor = "multi_thread")] async fn query_planner_redis_update_defer() { + // If this test fails and the cache key format changed you'll need to update + // the key here. Look at the top of the file for instructions on getting + // the new cache key. + // + // You first need to follow the process and update the key in + // `test_redis_query_plan_config_update`, and then update the key in this + // test. + // + // This test requires graphos license, so make sure you have + // "TEST_APOLLO_KEY" and "TEST_APOLLO_GRAPH_REF" env vars set, otherwise the + // test just passes locally. test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), - "plan:0:v2.9.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:e4376fe032160ce16399e520c6e815da6cb5cf4dc94a06175b86b64a9bf80201", + "plan:0:v2.9.0:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:d6a3d7807bb94cfb26be4daeb35e974680b53755658fafd4c921c70cec1b7c39", ) .await; } #[tokio::test(flavor = "multi_thread")] async fn query_planner_redis_update_type_conditional_fetching() { + // If this test fails and the cache key format changed you'll need to update + // the key here. Look at the top of the file for instructions on getting + // the new cache key. + // + // You first need to follow the process and update the key in + // `test_redis_query_plan_config_update`, and then update the key in this + // test. + // + // This test requires graphos license, so make sure you have + // "TEST_APOLLO_KEY" and "TEST_APOLLO_GRAPH_REF" env vars set, otherwise the + // test just passes locally. test_redis_query_plan_config_update( include_str!( "fixtures/query_planner_redis_config_update_type_conditional_fetching.router.yaml" ), - "plan:0:v2.9.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:83d899fcb42d2202c39fc8350289b8247021da00ecf3d844553c190c49410507", + "plan:0:v2.9.0:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:8991411cc7b66d9f62ab1e661f2ce9ccaf53b0d203a275e43ced9a8b6bba02dd", ) .await; } #[tokio::test(flavor = "multi_thread")] async fn query_planner_redis_update_reuse_query_fragments() { + // If this test fails and the cache key format changed you'll need to update + // the key here. Look at the top of the file for instructions on getting + // the new cache key. + // + // You first need to follow the process and update the key in + // `test_redis_query_plan_config_update`, and then update the key in this + // test. + // + // This test requires graphos license, so make sure you have + // "TEST_APOLLO_KEY" and "TEST_APOLLO_GRAPH_REF" env vars set, otherwise the + // test just passes locally. test_redis_query_plan_config_update( include_str!( "fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml" ), - "plan:0:v2.9.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:d48f92f892bd67071694c0538a7e657ff8e0c52e1718f475190c17b503e9e8c3", + "plan:0:v2.9.0:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:c05e89caeb8efc4e8233e8648099b33414716fe901e714416fd0f65a67867f07", ) .await; } @@ -981,9 +1047,10 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key if !graph_os_enabled() { return; } - // This test shows that the redis key changes when the query planner config changes. - // The test starts a router with a specific config, executes a query, and checks the redis cache key. - // Then it updates the config, executes the query again, and checks the redis cache key. + // This test shows that the redis key changes when the query planner config + // changes. The test starts a router with a specific config, executes a + // query, and checks the redis cache key. Then it updates the config, + // executes the query again, and checks the redis cache key. let mut router = IntegrationTest::builder() .config(include_str!( "fixtures/query_planner_redis_config_update.router.yaml" @@ -995,7 +1062,9 @@ 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.9.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:0966f1528d47cee30b6140a164be16148dd360ee10b87744991e9d35af8e8a27"; + // If the tests above are failing, this is the key that needs to be changed first. + let starting_key = "plan:0:v2.9.0:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:a52c81e3e2e47c8363fbcd2653e196431c15716acc51fce4f58d9368ac4c2d8d"; + 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/samples/README.md b/apollo-router/tests/samples/README.md index 9f1aa78f0d..c37c65e06b 100644 --- a/apollo-router/tests/samples/README.md +++ b/apollo-router/tests/samples/README.md @@ -1,6 +1,6 @@ # File based integration tests -This folder contains a serie of Router integration tests that can be defined entirely through a JSON file. Thos tests are able to start and stop a router, reload its schema or configiration, make requests and check the expected response. While we can make similar tests from inside the Router's code, these tests here are faster to write and modify because they do not require recompilations of the Router, at the cost of a slightly higher runtime cost. +This folder contains a series of Router integration tests that can be defined entirely through a JSON file. Thos tests are able to start and stop a router, reload its schema or configiration, make requests and check the expected response. While we can make similar tests from inside the Router's code, these tests here are faster to write and modify because they do not require recompilations of the Router, at the cost of a slightly higher runtime cost. ## How to write a test diff --git a/apollo-router/tests/samples/basic/query1/supergraph.graphql b/apollo-router/tests/samples/basic/query1/supergraph.graphql index 971174056a..c5a920730a 100644 --- a/apollo-router/tests/samples/basic/query1/supergraph.graphql +++ b/apollo-router/tests/samples/basic/query1/supergraph.graphql @@ -1,90 +1,124 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION +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 -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION +scalar join__FieldSet +scalar link__Import -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ +enum link__Purpose { SECURITY + EXECUTION } -scalar join__FieldSet - -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") -} -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + upc: String! } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! author: User @join__field(graph: REVIEWS, provides: "username") body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) product: Product @join__field(graph: REVIEWS) } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/samples/basic/query2/supergraph.graphql b/apollo-router/tests/samples/basic/query2/supergraph.graphql index 971174056a..1bd9f596ee 100644 --- a/apollo-router/tests/samples/basic/query2/supergraph.graphql +++ b/apollo-router/tests/samples/basic/query2/supergraph.graphql @@ -1,90 +1,125 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION - -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION +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 -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION +scalar join__FieldSet - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") } -scalar join__FieldSet +scalar link__Import -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") +enum link__Purpose { + SECURITY + EXECUTION } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + upc: String! } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ - author: User @join__field(graph: REVIEWS, provides: "username") +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) + author: User @join__field(graph: REVIEWS, provides: "username") product: Product @join__field(graph: REVIEWS) } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/supergraph.graphql b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/supergraph.graphql index 630e59c38b..ce3258e776 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/supergraph.graphql +++ b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/supergraph.graphql @@ -1,40 +1,59 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/tag/v0.3") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION - -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION - - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY -} +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet +scalar link__Import enum join__Graph { ACCOUNTS @join__graph(name: "invalidation-entity-key-accounts", url: "https://accounts.demo.starstuff.dev") @@ -42,49 +61,61 @@ enum join__Graph { PRODUCTS @join__graph(name: "invalidation-entity-key-products", url: "https://products.demo.starstuff.dev") REVIEWS @join__graph(name: "invalidation-entity-key-reviews", url: "https://reviews.demo.starstuff.dev") } -type Mutation { + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) + @join__type(graph: ACCOUNTS) { updateMyAccount: User @join__field(graph: ACCOUNTS) invalidateProductReview: Int @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + upc: String! } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + body: String + id: ID! + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/supergraph.graphql b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/supergraph.graphql index c8184433b1..e88cb8ecfa 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/supergraph.graphql +++ b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/supergraph.graphql @@ -1,40 +1,59 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/tag/v0.3") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION - -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION - - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY -} +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet +scalar link__Import enum join__Graph { ACCOUNTS @join__graph(name: "invalidation-subgraph-name-accounts", url: "https://accounts.demo.starstuff.dev") @@ -42,50 +61,63 @@ enum join__Graph { PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") } -type Mutation { + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) + @join__type(graph: ACCOUNTS) { updateMyAccount: User @join__field(graph: ACCOUNTS) createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + upc: String! } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + body: String + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) - reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + reviews: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/supergraph.graphql b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/supergraph.graphql index a9554a070d..75367a7939 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/supergraph.graphql +++ b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/supergraph.graphql @@ -1,40 +1,58 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/tag/v0.3") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA - -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION - -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION - - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY -} +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet +scalar link__Import enum join__Graph { ACCOUNTS @join__graph(name: "invalidation-subgraph-type-accounts", url: "https://accounts.demo.starstuff.dev") @@ -42,50 +60,63 @@ enum join__Graph { PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") } -type Mutation { + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) + @join__type(graph: ACCOUNTS) { updateMyAccount: User @join__field(graph: ACCOUNTS) createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + upc: String! } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + body: String + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) - reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + reviews: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/samples/enterprise/entity-cache/private/supergraph.graphql b/apollo-router/tests/samples/enterprise/entity-cache/private/supergraph.graphql index 1196414b6f..1f6a22ae3b 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/private/supergraph.graphql +++ b/apollo-router/tests/samples/enterprise/entity-cache/private/supergraph.graphql @@ -1,91 +1,129 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/tag/v0.3") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA - -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION +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 -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION +scalar join__FieldSet +scalar link__Import -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ +enum link__Purpose { SECURITY + EXECUTION } -scalar join__FieldSet - -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") -} -type Mutation { +type Mutation + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) + @join__type(graph: ACCOUNTS) { updateMyAccount: User @join__field(graph: ACCOUNTS) createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + upc: String! } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + body: String + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) - reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + reviews: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/samples/enterprise/query-planning-redis/supergraph.graphql b/apollo-router/tests/samples/enterprise/query-planning-redis/supergraph.graphql index 971174056a..9f9fedb1b8 100644 --- a/apollo-router/tests/samples/enterprise/query-planning-redis/supergraph.graphql +++ b/apollo-router/tests/samples/enterprise/query-planning-redis/supergraph.graphql @@ -1,90 +1,129 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/tag/v0.3") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA - -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION +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 -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION +scalar join__FieldSet +scalar link__Import -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ +enum link__Purpose { SECURITY + EXECUTION } -scalar join__FieldSet - -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") -} -type Mutation { +type Mutation + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) + @join__type(graph: ACCOUNTS) { + updateMyAccount: User @join__field(graph: ACCOUNTS) createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + upc: String! } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + body: String + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) - reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + reviews: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/snapshots/integration_tests__starstuff_supergraph_is_valid.snap b/apollo-router/tests/snapshots/integration_tests__starstuff_supergraph_is_valid.snap index dd48a351f6..71dbc40c36 100644 --- a/apollo-router/tests/snapshots/integration_tests__starstuff_supergraph_is_valid.snap +++ b/apollo-router/tests/snapshots/integration_tests__starstuff_supergraph_is_valid.snap @@ -2,90 +2,124 @@ source: apollo-router/tests/integration_tests.rs expression: "include_str!(\"../../examples/graphql/supergraph.graphql\")" --- - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, provides: join__FieldSet, requires: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT +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 -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on INTERFACE | OBJECT +scalar join__FieldSet -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +scalar link__Import + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) + recommendedProducts: [Product] @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) -} - -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION - - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY } - -scalar join__FieldSet - -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory" url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products" url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews" url: "https://reviews.demo.starstuff.dev") -} - diff --git a/docs/source/quickstart.mdx b/docs/source/quickstart.mdx index f46cb761ed..9e6649d1a7 100644 --- a/docs/source/quickstart.mdx +++ b/docs/source/quickstart.mdx @@ -4,12 +4,12 @@ subtitle: Run the router with GraphOS and Apollo-hosted subgraphs description: This quickstart tutorial walks you through installing the Apollo GraphOS Router or Apollo Router Core and running it with GraphOS and some example Apollo-hosted subgraphs. --- -import ElasticNotice from '../shared/elastic-notice.mdx'; +import ElasticNotice from "../shared/elastic-notice.mdx"; Hello! This tutorial walks you through installing the router (GraphOS Router or Apollo Router Core) and running it in with GraphOS and some example Apollo-hosted subgraphs. -> **This quickstart helps you run a _self-hosted_ instance of the router.** If you [create a cloud supergraph](/graphos/quickstart/cloud/) with Apollo GraphOS, Apollo provisions and hosts your supergraph's GraphOS -Router for you. +> **This quickstart helps you run a _self-hosted_ instance of the router.** If you [create a cloud supergraph](/graphos/quickstart/cloud/) with Apollo GraphOS, Apollo provisions and hosts your supergraph's GraphOS +> Router for you. > > Cloud supergraphs are recommended for organizations that don't need to host their router in their own infrastructure. @@ -17,10 +17,8 @@ Router for you. - ### Download options - #### Automatic download (Linux, OSX, WSL) If you have a bash-compatible terminal, you can download the latest version of the Apollo Router Core directly to your current directory with the following command: @@ -33,12 +31,12 @@ curl -sSL https://router.apollo.dev/download/nix/latest | sh Go to the Apollo Router Core's [GitHub Releases page](https://github.com/apollographql/router/releases) and download the latest `.tar.gz` file that matches your system. Currently, tarballs are available for the following: -* Linux (x86_64) -* Linux (aarch64) -* macOS (Apple Silicon) -* Windows (x86_64) +- Linux (x86_64) +- Linux (aarch64) +- macOS (Apple Silicon) +- Windows (x86_64) -If a tarball for your system or architecture isn't available, you can [build and run the router from source](https://github.com/apollographql/router/blob/HEAD/DEVELOPMENT.md#development-1). You can also [open an issue on GitHub](https://github.com/apollographql/router/issues/new/choose) to request the addition of new architectures. +If a tarball for your system or architecture isn't available, you can [build and run the router from source](https://github.com/apollographql/router/blob/HEAD/DEVELOPMENT.md#development-1). You can also [open an issue on GitHub](https://github.com/apollographql/router/issues/new/choose) to request the addition of new architectures. After downloading, extract the file by running the following from a new project directory, substituting the path to the tarball: @@ -106,77 +104,132 @@ This saves a `supergraph-schema.graphql` file with the following contents: ```graphql title="supergraph-schema.graphql" schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA - -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + scalar join__FieldSet enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory" url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products" url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews" url: "https://reviews.demo.starstuff.dev") + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) + recommendedProducts: [Product] @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } ``` @@ -216,6 +269,6 @@ Visit `http://127.0.0.1:4000` to open Apollo Sandbox, inspect your entire superg Now that you know how to run the router with a supergraph schema, you can: -* Set up [managed federation](/federation/managed-federation/overview) -* Learn about [additional configuration options](./configuration/overview) -* [Estimate the system resources needed to deploy the router](/technotes/TN0045-router_resource_estimator/). +- Set up [managed federation](/federation/managed-federation/overview) +- Learn about [additional configuration options](./configuration/overview) +- [Estimate the system resources needed to deploy the router](/technotes/TN0045-router_resource_estimator/). diff --git a/examples/graphql/federation2.graphql b/examples/graphql/federation2.graphql deleted file mode 100644 index 7ec10a34d8..0000000000 --- a/examples/graphql/federation2.graphql +++ /dev/null @@ -1,134 +0,0 @@ - -schema - @core(feature: "https://specs.apollo.dev/core/v0.2") - @core(feature: "https://specs.apollo.dev/join/v0.2", for: EXECUTION) -{ - query: Query -} - -directive @core(feature: String!, as: String, for: core__Purpose) repeatable on SCHEMA - -directive @join__field(graph: join__Graph!, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION - -directive @join__graph(name: String!, url: String!) on ENUM_VALUE - -directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE - -directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR - -enum core__Purpose { - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY - - """ - `EXECUTION` features provide metadata necessary for operation execution. - """ - EXECUTION -} - -type DeliveryEstimates - @join__type(graph: INVENTORY) -{ - estimatedDelivery: String - fastestDelivery: String -} - -scalar join__FieldSet - -enum join__Graph { - USERS @join__graph(name: "users", url: "http://localhost:4001") - PANDAS @join__graph(name: "pandas", url: "http://localhost:4002") - PRODUCTS @join__graph(name: "products", url: "http://localhost:4003") - INVENTORY @join__graph(name: "inventory", url: "http://localhost:4004/graphql") -} - -type Panda - @join__type(graph: PANDAS) -{ - name: ID! - favoriteFood: String -} - -type Product implements ProductItf & SkuItf - @join__implements(graph: INVENTORY, interface: "ProductItf") - @join__implements(graph: PRODUCTS, interface: "ProductItf") - @join__implements(graph: PRODUCTS, interface: "SkuItf") - @join__type(graph: INVENTORY, key: "id") - @join__type(graph: PRODUCTS, key: "id") - @join__type(graph: PRODUCTS, key: "sku package") - @join__type(graph: PRODUCTS, key: "sku variation { id }") -{ - id: ID! - dimensions: ProductDimension @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) - delivery(zip: String): DeliveryEstimates @join__field(graph: INVENTORY, requires: "dimensions { size weight }") - sku: String @join__field(graph: PRODUCTS) - package: String @join__field(graph: PRODUCTS) - variation: ProductVariation @join__field(graph: PRODUCTS) - createdBy: User @join__field(graph: PRODUCTS) -} - -type ProductDimension - @join__type(graph: INVENTORY) - @join__type(graph: PRODUCTS) -{ - size: String - weight: Float -} - -interface ProductItf implements SkuItf - @join__implements(graph: PRODUCTS, interface: "SkuItf") - @join__type(graph: INVENTORY) - @join__type(graph: PRODUCTS) -{ - id: ID! - dimensions: ProductDimension - delivery(zip: String): DeliveryEstimates @join__field(graph: INVENTORY) - sku: String @join__field(graph: PRODUCTS) - package: String @join__field(graph: PRODUCTS) - variation: ProductVariation @join__field(graph: PRODUCTS) - createdBy: User @join__field(graph: PRODUCTS) -} - -type ProductVariation - @join__type(graph: PRODUCTS) -{ - id: ID! -} - -type Query - @join__type(graph: INVENTORY) - @join__type(graph: PANDAS) - @join__type(graph: PRODUCTS) - @join__type(graph: USERS) -{ - allPandas: [Panda] @join__field(graph: PANDAS) - panda(name: ID!): Panda @join__field(graph: PANDAS) - allProducts: [ProductItf] @join__field(graph: PRODUCTS) - product(id: ID!): ProductItf @join__field(graph: PRODUCTS) -} - -enum ShippingClass - @join__type(graph: INVENTORY) - @join__type(graph: PRODUCTS) -{ - STANDARD - EXPRESS - OVERNIGHT -} - -interface SkuItf - @join__type(graph: PRODUCTS) -{ - sku: String -} - -type User - @join__type(graph: PRODUCTS, key: "email") - @join__type(graph: USERS, key: "email") -{ - email: ID! - totalProductsCreated: Int - name: String @join__field(graph: USERS) -} \ No newline at end of file diff --git a/examples/graphql/local.graphql b/examples/graphql/local.graphql index 3090cbec59..5e39bb3d85 100644 --- a/examples/graphql/local.graphql +++ b/examples/graphql/local.graphql @@ -1,84 +1,120 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") { + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query - mutation: Mutation subscription: Subscription + mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE directive @join__field( graph: join__Graph requires: join__FieldSet provides: join__FieldSet -) on FIELD_DEFINITION + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE directive @join__type( graph: join__Graph! key: join__FieldSet -) repeatable on OBJECT | INTERFACE + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet +scalar link__Import enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001") - INVENTORY @join__graph(name: "inventory", url: "http://localhost:4004") - PRODUCTS @join__graph(name: "products", url: "http://localhost:4003") - REVIEWS @join__graph(name: "reviews", url: "http://localhost:4002") + ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001/graphql") + INVENTORY + @join__graph(name: "inventory", url: "http://localhost:4002/graphql") + PRODUCTS @join__graph(name: "products", url: "http://localhost:4003/graphql") + REVIEWS @join__graph(name: "reviews", url: "http://localhost:4004/graphql") } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review - @join__field(graph: REVIEWS) +enum link__Purpose { + SECURITY + EXECUTION } -type Subscription { +type Subscription @join__type(graph: ACCOUNTS) { userWasCreated(intervalMs: Int, nbEvents: Int): User @join__field(graph: ACCOUNTS) } +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) +} + type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") { +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! author: User @join__field(graph: REVIEWS, provides: "username") body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) product: Product @join__field(graph: REVIEWS) } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") @join__type(graph: REVIEWS, key: "id") { - id: ID! @join__field(graph: ACCOUNTS) + id: ID! name: String @join__field(graph: ACCOUNTS) createdAt: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } diff --git a/examples/graphql/supergraph-fed1.graphql b/examples/graphql/supergraph-fed1.graphql new file mode 100644 index 0000000000..dadcc318f0 --- /dev/null +++ b/examples/graphql/supergraph-fed1.graphql @@ -0,0 +1,90 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.2") + @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) { + query: Query + mutation: Mutation +} + +directive @core( + as: String + feature: String! + for: core__Purpose +) repeatable on SCHEMA + +directive @join__field( + graph: join__Graph + provides: join__FieldSet + requires: join__FieldSet +) on FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT + +directive @join__type( + graph: join__Graph! + key: join__FieldSet +) repeatable on INTERFACE | OBJECT + +type Mutation { + createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) +} + +type Product + @join__owner(graph: PRODUCTS) + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean @join__field(graph: INVENTORY) + name: String @join__field(graph: PRODUCTS) + price: Int @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) + reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + upc: String! @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: PRODUCTS) +} + +type Query { + me: User @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) +} + +type Review + @join__owner(graph: REVIEWS) + @join__type(graph: REVIEWS, key: "id") { + author: User @join__field(graph: REVIEWS, provides: "username") + body: String @join__field(graph: REVIEWS) + id: ID! @join__field(graph: REVIEWS) + product: Product @join__field(graph: REVIEWS) +} + +type User + @join__owner(graph: ACCOUNTS) + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! @join__field(graph: ACCOUNTS) + name: String @join__field(graph: ACCOUNTS) + reviews: [Review] @join__field(graph: REVIEWS) + username: String @join__field(graph: ACCOUNTS) +} + +enum core__Purpose { + EXECUTION + SECURITY +} + +scalar join__FieldSet + +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") + INVENTORY + @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") +} diff --git a/examples/graphql/supergraph-fed2.graphql b/examples/graphql/supergraph-fed2.graphql deleted file mode 100644 index 504fbbaafb..0000000000 --- a/examples/graphql/supergraph-fed2.graphql +++ /dev/null @@ -1,98 +0,0 @@ -schema - @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) -{ - query: Query - mutation: Mutation -} - -directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE - -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION - -directive @join__graph(name: String!, url: String!) on ENUM_VALUE - -directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE - -directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR - -directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION - -directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA - -scalar join__FieldSet - -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") - INVENTORY @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev/") - PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") - REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") -} - -scalar link__Import - -enum link__Purpose { - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY - - """ - `EXECUTION` features provide metadata necessary for operation execution. - """ - EXECUTION -} - -type Mutation - @join__type(graph: PRODUCTS) - @join__type(graph: REVIEWS) -{ - createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) - createReview(upc: ID!, id: ID!, body: String): Review @join__field(graph: REVIEWS) -} - -type Product - @join__type(graph: ACCOUNTS, key: "upc", extension: true) - @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: PRODUCTS, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - upc: String! - weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) - price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) - inStock: Boolean @join__field(graph: INVENTORY) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - name: String @join__field(graph: PRODUCTS) - reviews: [Review] @join__field(graph: REVIEWS) - reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) -} - -type Query - @join__type(graph: ACCOUNTS) - @join__type(graph: INVENTORY) - @join__type(graph: PRODUCTS) - @join__type(graph: REVIEWS) -{ - me: User @join__field(graph: ACCOUNTS) - recommendedProducts: [Product] @join__field(graph: ACCOUNTS) - topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) -} - -type Review - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! - body: String - author: User @join__field(graph: REVIEWS, provides: "username") - product: Product -} - -type User - @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! - name: String @join__field(graph: ACCOUNTS) - username: String @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS, external: true) - reviews: [Review] @join__field(graph: REVIEWS) -} diff --git a/examples/graphql/supergraph.graphql b/examples/graphql/supergraph.graphql index 08a37b12e1..0b1bdf1414 100644 --- a/examples/graphql/supergraph.graphql +++ b/examples/graphql/supergraph.graphql @@ -1,86 +1,121 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, provides: join__FieldSet, requires: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on INTERFACE | OBJECT +scalar link__Import -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) + recommendedProducts: [Product] @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) -} - -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION - - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY -} - -scalar join__FieldSet - -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory" url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products" url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews" url: "https://reviews.demo.starstuff.dev") } diff --git a/fuzz/supergraph-fed2.graphql b/fuzz/supergraph-fed2.graphql deleted file mode 100644 index f588155d30..0000000000 --- a/fuzz/supergraph-fed2.graphql +++ /dev/null @@ -1,141 +0,0 @@ -schema - @core(feature: "https://specs.apollo.dev/core/v0.2") - @core(feature: "https://specs.apollo.dev/join/v0.2", for: EXECUTION) { - query: Query -} - -directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT -directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - -directive @core( - feature: String! - as: String - for: core__Purpose -) repeatable on SCHEMA - -directive @join__field( - graph: join__Graph! - requires: join__FieldSet - provides: join__FieldSet - type: String - external: Boolean -) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION - -directive @join__graph(name: String!, url: String!) on ENUM_VALUE - -directive @join__implements( - graph: join__Graph! - interface: String! -) repeatable on OBJECT | INTERFACE - -directive @join__type( - graph: join__Graph! - key: join__FieldSet - extension: Boolean! = false - resolvable: Boolean! = true -) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR - -enum core__Purpose { - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY - - """ - `EXECUTION` features provide metadata necessary for operation execution. - """ - EXECUTION -} - -type DeliveryEstimates @join__type(graph: INVENTORY) { - estimatedDelivery: String - fastestDelivery: String -} - -scalar join__FieldSet - -enum join__Graph { - INVENTORY - @join__graph(name: "inventory", url: "http://localhost:4004/graphql") - PANDAS @join__graph(name: "pandas", url: "http://localhost:4002/graphql") - PRODUCTS @join__graph(name: "products", url: "http://localhost:4003/graphql") - USERS @join__graph(name: "users", url: "http://localhost:4001/graphql") -} - -type Panda @join__type(graph: PANDAS) { - name: ID! - favoriteFood: String -} - -type Product implements ProductItf & SkuItf - @join__implements(graph: INVENTORY, interface: "ProductItf") - @join__implements(graph: PRODUCTS, interface: "ProductItf") - @join__implements(graph: PRODUCTS, interface: "SkuItf") - @join__type(graph: INVENTORY, key: "id") - @join__type(graph: PRODUCTS, key: "id") - @join__type(graph: PRODUCTS, key: "sku package") - @join__type(graph: PRODUCTS, key: "sku variation { id }") { - id: ID! - dimensions: ProductDimension - @join__field(graph: INVENTORY, external: true) - @join__field(graph: PRODUCTS) - delivery(zip: String): DeliveryEstimates - @join__field(graph: INVENTORY, requires: "dimensions { size weight }") - sku: String @join__field(graph: PRODUCTS) - package: String @join__field(graph: PRODUCTS) - variation: ProductVariation @join__field(graph: PRODUCTS) - createdBy: User @join__field(graph: PRODUCTS) -} - -type ProductDimension - @join__type(graph: INVENTORY) - @join__type(graph: PRODUCTS) { - size: String - weight: Float -} - -interface ProductItf implements SkuItf - @join__implements(graph: PRODUCTS, interface: "SkuItf") - @join__type(graph: INVENTORY) - @join__type(graph: PRODUCTS) { - id: ID! - dimensions: ProductDimension - delivery(zip: String): DeliveryEstimates @join__field(graph: INVENTORY) - sku: String @join__field(graph: PRODUCTS) - package: String @join__field(graph: PRODUCTS) - variation: ProductVariation @join__field(graph: PRODUCTS) - createdBy: User @join__field(graph: PRODUCTS) -} - -type ProductVariation @join__type(graph: PRODUCTS) { - id: ID! -} - -type Query - @join__type(graph: INVENTORY) - @join__type(graph: PANDAS) - @join__type(graph: PRODUCTS) - @join__type(graph: USERS) { - allPandas: [Panda] @join__field(graph: PANDAS) - panda(name: ID!): Panda @join__field(graph: PANDAS) - allProducts: [ProductItf] @join__field(graph: PRODUCTS) - product(id: ID!): ProductItf @join__field(graph: PRODUCTS) -} - -enum ShippingClass @join__type(graph: INVENTORY) @join__type(graph: PRODUCTS) { - STANDARD - EXPRESS - OVERNIGHT -} - -interface SkuItf @join__type(graph: PRODUCTS) { - sku: String -} - -type User - @join__type(graph: PRODUCTS, key: "email") - @join__type(graph: USERS, key: "email") { - email: ID! - totalProductsCreated: Int - name: String @join__field(graph: USERS) -} diff --git a/fuzz/supergraph.graphql b/fuzz/supergraph.graphql index 63802405f1..112d8f88f0 100644 --- a/fuzz/supergraph.graphql +++ b/fuzz/supergraph.graphql @@ -1,86 +1,147 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") { + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA - directive @join__field( graph: join__Graph requires: join__FieldSet provides: join__FieldSet -) on FIELD_DEFINITION + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE directive @join__type( graph: join__Graph! key: join__FieldSet -) repeatable on OBJECT | INTERFACE + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT -directive @deprecated( - reason: String = "No longer supported" -) on FIELD_DEFINITION | ENUM_VALUE +scalar join__FieldSet +scalar link__Import -# Uncomment if you want to reproduce the bug with the order of skip/include directives -# directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT -# directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY -scalar join__FieldSet + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "http://subgraphs:4001/graphql") INVENTORY - @join__graph(name: "inventory", url: "http://subgraphs:4004/graphql") - PRODUCTS @join__graph(name: "products", url: "http://subgraphs:4003/graphql") - REVIEWS @join__graph(name: "reviews", url: "http://subgraphs:4002/graphql") + @join__graph(name: "inventory", url: "http://localhost:4004/graphql") + PANDAS @join__graph(name: "pandas", url: "http://localhost:4002/graphql") + PRODUCTS @join__graph(name: "products", url: "http://localhost:4003/graphql") + USERS @join__graph(name: "users", url: "http://localhost:4001/graphql") +} + +type DeliveryEstimates @join__type(graph: INVENTORY) { + estimatedDelivery: String + fastestDelivery: String +} + +type Panda @join__type(graph: PANDAS) { + name: ID! + favoriteFood: String +} + +type Product implements ProductItf & SkuItf + @join__implements(graph: INVENTORY, interface: "ProductItf") + @join__implements(graph: PRODUCTS, interface: "ProductItf") + @join__implements(graph: PRODUCTS, interface: "SkuItf") + @join__type(graph: INVENTORY, key: "id") + @join__type(graph: PRODUCTS, key: "id") + @join__type(graph: PRODUCTS, key: "sku package") + @join__type(graph: PRODUCTS, key: "sku variation { id }") { + id: ID! + dimensions: ProductDimension + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + delivery(zip: String): DeliveryEstimates + @join__field(graph: INVENTORY, requires: "dimensions { size weight }") + sku: String @join__field(graph: PRODUCTS) + package: String @join__field(graph: PRODUCTS) + variation: ProductVariation @join__field(graph: PRODUCTS) + createdBy: User @join__field(graph: PRODUCTS) +} + +type ProductDimension + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) { + size: String + weight: Float +} + +interface ProductItf implements SkuItf + @join__implements(graph: PRODUCTS, interface: "SkuItf") + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) { + id: ID! + dimensions: ProductDimension + delivery(zip: String): DeliveryEstimates @join__field(graph: INVENTORY) + sku: String @join__field(graph: PRODUCTS) + package: String @join__field(graph: PRODUCTS) + variation: ProductVariation @join__field(graph: PRODUCTS) + createdBy: User @join__field(graph: PRODUCTS) } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review - @join__field(graph: REVIEWS) +type ProductVariation @join__type(graph: PRODUCTS) { + id: ID! } -type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") - @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") { - inStock: Boolean @join__field(graph: INVENTORY) - name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) - reviews: [Review] @join__field(graph: REVIEWS) - reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) +type Query + @join__type(graph: INVENTORY) + @join__type(graph: PANDAS) + @join__type(graph: PRODUCTS) + @join__type(graph: USERS) { + allPandas: [Panda] @join__field(graph: PANDAS) + panda(name: ID!): Panda @join__field(graph: PANDAS) + allProducts: [ProductItf] @join__field(graph: PRODUCTS) + product(id: ID!): ProductItf @join__field(graph: PRODUCTS) } -type Query { - me: User @join__field(graph: ACCOUNTS) - topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) +enum ShippingClass @join__type(graph: INVENTORY) @join__type(graph: PRODUCTS) { + STANDARD + EXPRESS + OVERNIGHT } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") { - author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) +interface SkuItf @join__type(graph: PRODUCTS) { + sku: String } type User - @join__owner(graph: ACCOUNTS) - @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") { - id: ID! @join__field(graph: ACCOUNTS) - name: String @join__field(graph: ACCOUNTS) - reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) + @join__type(graph: PRODUCTS, key: "email") + @join__type(graph: USERS, key: "email") { + email: ID! + totalProductsCreated: Int + name: String @join__field(graph: USERS) } From 855cf6cc0757ca6176970ddf3ae8c98c87c632d1 Mon Sep 17 00:00:00 2001 From: Bryn Cooke Date: Thu, 12 Sep 2024 16:16:46 +0100 Subject: [PATCH 19/56] Fix query plan cache gauges that stopped working after hot reload (#5996) Co-authored-by: bryn --- .changesets/fix_bryn_fix_gauges.md | 9 +++ apollo-router/src/cache/mod.rs | 4 ++ apollo-router/src/cache/storage.rs | 65 ++++++++++--------- apollo-router/src/metrics/aggregation.rs | 3 - .../query_planner/caching_query_planner.rs | 6 ++ .../src/services/supergraph/service.rs | 11 ++-- .../tests/integration/telemetry/metrics.rs | 18 +++++ 7 files changed, 75 insertions(+), 41 deletions(-) create mode 100644 .changesets/fix_bryn_fix_gauges.md diff --git a/.changesets/fix_bryn_fix_gauges.md b/.changesets/fix_bryn_fix_gauges.md new file mode 100644 index 0000000000..a53743ecd8 --- /dev/null +++ b/.changesets/fix_bryn_fix_gauges.md @@ -0,0 +1,9 @@ +### Query plan cache gauges stop working after hot reload ([PR #5996](https://github.com/apollographql/router/pull/5996)) + +When the router reloads the schema or config, the query plan cache gauges stop working. These are: +* `apollo.router.cache.storage.estimated_size` +* `apollo_router_cache_size` + +The gauges will now continue to function after a router hot reload. + +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5996 diff --git a/apollo-router/src/cache/mod.rs b/apollo-router/src/cache/mod.rs index 6e1ef01cb8..04b578a437 100644 --- a/apollo-router/src/cache/mod.rs +++ b/apollo-router/src/cache/mod.rs @@ -137,6 +137,10 @@ where pub(crate) fn in_memory_cache(&self) -> InMemoryCache { self.storage.in_memory_cache() } + + pub(crate) fn activate(&self) { + self.storage.activate() + } } pub(crate) struct Entry { diff --git a/apollo-router/src/cache/storage.rs b/apollo-router/src/cache/storage.rs index 7cfa37a0ad..15f3b3dc8a 100644 --- a/apollo-router/src/cache/storage.rs +++ b/apollo-router/src/cache/storage.rs @@ -8,7 +8,6 @@ use std::sync::Arc; use lru::LruCache; use opentelemetry::metrics::MeterProvider; -use opentelemetry_api::metrics::Meter; use opentelemetry_api::metrics::ObservableGauge; use opentelemetry_api::metrics::Unit; use opentelemetry_api::KeyValue; @@ -53,13 +52,14 @@ pub(crate) type InMemoryCache = Arc>>; // a suitable implementation. #[derive(Clone)] pub(crate) struct CacheStorage { - caller: String, + caller: &'static str, inner: Arc>>, redis: Option, cache_size: Arc, cache_estimated_storage: Arc, - _cache_size_gauge: ObservableGauge, - _cache_estimated_storage_gauge: ObservableGauge, + // It's OK for these to be mutexes as they are only initialized once + cache_size_gauge: Arc>>>, + cache_estimated_storage_gauge: Arc>>>, } impl CacheStorage @@ -72,18 +72,12 @@ where config: Option, caller: &'static str, ) -> Result { - // Because calculating the cache size is expensive we do this as we go rather than iterating. This means storing the values for the gauges - let meter: opentelemetry::metrics::Meter = metrics::meter_provider().meter(METER_NAME); - let (cache_size, cache_size_gauge) = Self::create_cache_size_gauge(&meter, caller); - let (cache_estimated_storage, cache_estimated_storage_gauge) = - Self::create_cache_estimated_storage_size_gauge(&meter, caller); - Ok(Self { - _cache_size_gauge: cache_size_gauge, - _cache_estimated_storage_gauge: cache_estimated_storage_gauge, - cache_size, - cache_estimated_storage, - caller: caller.to_string(), + cache_size_gauge: Default::default(), + cache_estimated_storage_gauge: Default::default(), + cache_size: Default::default(), + cache_estimated_storage: Default::default(), + caller, inner: Arc::new(Mutex::new(LruCache::new(max_capacity))), redis: if let Some(config) = config { let required_to_start = config.required_to_start; @@ -107,13 +101,11 @@ where }) } - fn create_cache_size_gauge( - meter: &Meter, - caller: &'static str, - ) -> (Arc, ObservableGauge) { - let current_cache_size = Arc::new(AtomicI64::new(0)); - let current_cache_size_for_gauge = current_cache_size.clone(); - let cache_size_gauge = meter + fn create_cache_size_gauge(&self) -> ObservableGauge { + let meter: opentelemetry::metrics::Meter = metrics::meter_provider().meter(METER_NAME); + let current_cache_size_for_gauge = self.cache_size.clone(); + let caller = self.caller; + meter // TODO move to dot naming convention .i64_observable_gauge("apollo_router_cache_size") .with_description("Cache size") @@ -126,16 +118,13 @@ where ], ) }) - .init(); - (current_cache_size, cache_size_gauge) + .init() } - fn create_cache_estimated_storage_size_gauge( - meter: &Meter, - caller: &'static str, - ) -> (Arc, ObservableGauge) { - let cache_estimated_storage = Arc::new(AtomicI64::new(0)); - let cache_estimated_storage_for_gauge = cache_estimated_storage.clone(); + fn create_cache_estimated_storage_size_gauge(&self) -> ObservableGauge { + let meter: opentelemetry::metrics::Meter = metrics::meter_provider().meter(METER_NAME); + let cache_estimated_storage_for_gauge = self.cache_estimated_storage.clone(); + let caller = self.caller; let cache_estimated_storage_gauge = meter .i64_observable_gauge("apollo.router.cache.storage.estimated_size") .with_description("Estimated cache storage") @@ -154,7 +143,7 @@ where } }) .init(); - (cache_estimated_storage, cache_estimated_storage_gauge) + cache_estimated_storage_gauge } /// `init_from_redis` is called with values newly deserialized from Redis cache @@ -292,6 +281,17 @@ where pub(crate) async fn len(&self) -> usize { self.inner.lock().await.len() } + + pub(crate) fn activate(&self) { + // Gauges MUST be created after the meter provider is initialized + // This means that on reload we need a non-fallible way to recreate the gauges, hence this function. + *self.cache_size_gauge.lock().expect("lock poisoned") = + Some(self.create_cache_size_gauge()); + *self + .cache_estimated_storage_gauge + .lock() + .expect("lock poisoned") = Some(self.create_cache_estimated_storage_size_gauge()); + } } enum CacheStorageName { @@ -350,6 +350,7 @@ mod test { CacheStorage::new(NonZeroUsize::new(10).unwrap(), None, "test") .await .unwrap(); + cache.activate(); cache.insert("test".to_string(), Stuff {}).await; assert_gauge!( @@ -385,6 +386,7 @@ mod test { CacheStorage::new(NonZeroUsize::new(10).unwrap(), None, "test") .await .unwrap(); + cache.activate(); cache.insert("test".to_string(), Stuff {}).await; // This metric won't exist @@ -418,6 +420,7 @@ mod test { CacheStorage::new(NonZeroUsize::new(1).unwrap(), None, "test") .await .unwrap(); + cache.activate(); cache .insert( diff --git a/apollo-router/src/metrics/aggregation.rs b/apollo-router/src/metrics/aggregation.rs index 53f36aee88..d136f82e5a 100644 --- a/apollo-router/src/metrics/aggregation.rs +++ b/apollo-router/src/metrics/aggregation.rs @@ -99,9 +99,6 @@ pub(crate) enum InstrumentWrapper { F64Histogram { _keep_alive: Arc>, }, - U64Gauge { - _keep_alive: Arc>, - }, } #[derive(Eq, PartialEq, Hash)] diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index bee5c9a8b5..9a4aec2e77 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -85,6 +85,12 @@ pub(crate) struct CachingQueryPlanner { legacy_introspection_caching: bool, } +impl CachingQueryPlanner { + pub(crate) fn activate(&self) { + self.cache.activate() + } +} + fn init_query_plan_from_redis( subgraph_schemas: &SubgraphSchemas, cache_entry: &mut Result>, diff --git a/apollo-router/src/services/supergraph/service.rs b/apollo-router/src/services/supergraph/service.rs index 136f4e9063..a77282d2db 100644 --- a/apollo-router/src/services/supergraph/service.rs +++ b/apollo-router/src/services/supergraph/service.rs @@ -798,6 +798,7 @@ impl PluggableSupergraphServiceBuilder { let schema = self.planner.schema(); let subgraph_schemas = self.planner.subgraph_schemas(); + let query_planner_service = CachingQueryPlanner::new( self.planner, schema.clone(), @@ -815,13 +816,9 @@ impl PluggableSupergraphServiceBuilder { } } - /*for (_, service) in self.subgraph_services.iter_mut() { - if let Some(subgraph) = - (service as &mut dyn std::any::Any).downcast_mut::() - { - subgraph.client_factory.plugins = plugins.clone(); - } - }*/ + // We need a non-fallible hook so that once we know we are going live with a pipeline we do final initialization. + // For now just shoe-horn something in, but if we ever reintroduce the query planner hook in plugins and activate then this can be made clean. + query_planner_service.activate(); let subgraph_service_factory = Arc::new(SubgraphServiceFactory::new( self.subgraph_services diff --git a/apollo-router/tests/integration/telemetry/metrics.rs b/apollo-router/tests/integration/telemetry/metrics.rs index f6d6e56417..f2ba74b272 100644 --- a/apollo-router/tests/integration/telemetry/metrics.rs +++ b/apollo-router/tests/integration/telemetry/metrics.rs @@ -220,3 +220,21 @@ async fn test_graphql_metrics() { .assert_metrics_contains(r#"custom_histogram_sum{graphql_field_name="topProducts",graphql_field_type="Product",graphql_type_name="Query",otel_scope_name="apollo/router"} 3"#, None) .await; } + +#[tokio::test(flavor = "multi_thread")] +async fn test_gauges_on_reload() { + let mut router = IntegrationTest::builder() + .config(include_str!("fixtures/no-telemetry.router.yaml")) + .build() + .await; + + router.start().await; + router.assert_started().await; + router.execute_default_query().await; + router.update_config(PROMETHEUS_CONFIG).await; + router.assert_reloaded().await; + router.execute_default_query().await; + router + .assert_metrics_contains(r#"apollo_router_cache_storage_estimated_size{kind="query planner",type="memory",otel_scope_name="apollo/router"} "#, None) + .await; +} From 5f48a8f294fc52b381b70e34b341b95741846040 Mon Sep 17 00:00:00 2001 From: Bryn Cooke Date: Fri, 13 Sep 2024 10:25:34 +0100 Subject: [PATCH 20/56] Fix v8 gauges on hot reload (#5999) Co-authored-by: bryn --- .changesets/fix_bryn_fix_gauges.md | 9 ++- .../bridge_query_planner_pool.rs | 72 +++++++++++-------- .../query_planner/caching_query_planner.rs | 11 ++- .../tests/integration/telemetry/metrics.rs | 18 +++++ 4 files changed, 70 insertions(+), 40 deletions(-) diff --git a/.changesets/fix_bryn_fix_gauges.md b/.changesets/fix_bryn_fix_gauges.md index a53743ecd8..107f201010 100644 --- a/.changesets/fix_bryn_fix_gauges.md +++ b/.changesets/fix_bryn_fix_gauges.md @@ -1,9 +1,12 @@ -### Query plan cache gauges stop working after hot reload ([PR #5996](https://github.com/apollographql/router/pull/5996)) +### Gauges stop working after hot reload ([PR #5996](https://github.com/apollographql/router/pull/5996), [PR #5999](https://github.com/apollographql/router/pull/5999)) -When the router reloads the schema or config, the query plan cache gauges stop working. These are: +When the router reloads the schema or config, some gauges stopped working. These were: * `apollo.router.cache.storage.estimated_size` * `apollo_router_cache_size` +* `apollo.router.v8.heap.used` +* `apollo.router.v8.heap.total` +* `apollo.router.query_planning.queued` The gauges will now continue to function after a router hot reload. -By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5996 +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5996 and https://github.com/apollographql/router/pull/5999 diff --git a/apollo-router/src/query_planner/bridge_query_planner_pool.rs b/apollo-router/src/query_planner/bridge_query_planner_pool.rs index b27ac14087..5c4c766218 100644 --- a/apollo-router/src/query_planner/bridge_query_planner_pool.rs +++ b/apollo-router/src/query_planner/bridge_query_planner_pool.rs @@ -3,6 +3,7 @@ use std::num::NonZeroUsize; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; use std::sync::Arc; +use std::sync::Mutex; use std::time::Instant; use apollo_compiler::validation::Valid; @@ -12,7 +13,6 @@ use futures::future::BoxFuture; use opentelemetry::metrics::MeterProvider; use opentelemetry::metrics::ObservableGauge; use opentelemetry::metrics::Unit; -use opentelemetry_api::metrics::Meter; use router_bridge::planner::Planner; use tokio::sync::oneshot; use tokio::task::JoinSet; @@ -41,11 +41,11 @@ pub(crate) struct BridgeQueryPlannerPool { )>, schema: Arc, subgraph_schemas: Arc>>>, - _pool_size_gauge: opentelemetry::metrics::ObservableGauge, + pool_size_gauge: Arc>>>, v8_heap_used: Arc, - _v8_heap_used_gauge: ObservableGauge, + v8_heap_used_gauge: Arc>>>, v8_heap_total: Arc, - _v8_heap_total_gauge: ObservableGauge, + v8_heap_total_gauge: Arc>>>, } impl BridgeQueryPlannerPool { @@ -119,17 +119,8 @@ impl BridgeQueryPlannerPool { } }); } - let sender_for_gauge = sender.clone(); - let meter = meter_provider().meter("apollo/router"); - let pool_size_gauge = meter - .u64_observable_gauge("apollo.router.query_planning.queued") - .with_description("Number of queries waiting to be planned") - .with_unit(Unit::new("query")) - .with_callback(move |m| m.observe(sender_for_gauge.len() as u64, &[])) - .init(); - - let (v8_heap_used, _v8_heap_used_gauge) = Self::create_heap_used_gauge(&meter); - let (v8_heap_total, _v8_heap_total_gauge) = Self::create_heap_total_gauge(&meter); + let v8_heap_used: Arc = Default::default(); + let v8_heap_total: Arc = Default::default(); // initialize v8 metrics if let Some(bridge_query_planner) = js_planners.first().cloned() { @@ -146,17 +137,28 @@ impl BridgeQueryPlannerPool { sender, schema, subgraph_schemas, - _pool_size_gauge: pool_size_gauge, + pool_size_gauge: Default::default(), v8_heap_used, - _v8_heap_used_gauge, + v8_heap_used_gauge: Default::default(), v8_heap_total, - _v8_heap_total_gauge, + v8_heap_total_gauge: Default::default(), }) } - fn create_heap_used_gauge(meter: &Meter) -> (Arc, ObservableGauge) { - let current_heap_used = Arc::new(AtomicU64::new(0)); - let current_heap_used_for_gauge = current_heap_used.clone(); + fn create_pool_size_gauge(&self) -> ObservableGauge { + let sender = self.sender.clone(); + let meter = meter_provider().meter("apollo/router"); + meter + .u64_observable_gauge("apollo.router.query_planning.queued") + .with_description("Number of queries waiting to be planned") + .with_unit(Unit::new("query")) + .with_callback(move |m| m.observe(sender.len() as u64, &[])) + .init() + } + + fn create_heap_used_gauge(&self) -> ObservableGauge { + let meter = meter_provider().meter("apollo/router"); + let current_heap_used_for_gauge = self.v8_heap_used.clone(); let heap_used_gauge = meter .u64_observable_gauge("apollo.router.v8.heap.used") .with_description("V8 heap used, in bytes") @@ -165,12 +167,12 @@ impl BridgeQueryPlannerPool { i.observe(current_heap_used_for_gauge.load(Ordering::SeqCst), &[]) }) .init(); - (current_heap_used, heap_used_gauge) + heap_used_gauge } - fn create_heap_total_gauge(meter: &Meter) -> (Arc, ObservableGauge) { - let current_heap_total = Arc::new(AtomicU64::new(0)); - let current_heap_total_for_gauge = current_heap_total.clone(); + fn create_heap_total_gauge(&self) -> ObservableGauge { + let meter = meter_provider().meter("apollo/router"); + let current_heap_total_for_gauge = self.v8_heap_total.clone(); let heap_total_gauge = meter .u64_observable_gauge("apollo.router.v8.heap.total") .with_description("V8 heap total, in bytes") @@ -179,7 +181,7 @@ impl BridgeQueryPlannerPool { i.observe(current_heap_total_for_gauge.load(Ordering::SeqCst), &[]) }) .init(); - (current_heap_total, heap_total_gauge) + heap_total_gauge } pub(crate) fn js_planners(&self) -> Vec>> { @@ -207,6 +209,16 @@ impl BridgeQueryPlannerPool { v8_heap_total.store(metrics.heap_total, Ordering::SeqCst); } } + + pub(super) fn activate(&self) { + // Gauges MUST be initialized after a meter provider is created. + // When a hot reload happens this means that the gauges must be re-initialized. + *self.pool_size_gauge.lock().expect("lock poisoned") = Some(self.create_pool_size_gauge()); + *self.v8_heap_used_gauge.lock().expect("lock poisoned") = + Some(self.create_heap_used_gauge()); + *self.v8_heap_total_gauge.lock().expect("lock poisoned") = + Some(self.create_heap_total_gauge()); + } } impl tower::Service for BridgeQueryPlannerPool { @@ -235,13 +247,10 @@ impl tower::Service for BridgeQueryPlannerPool { let get_metrics_future = if let Some(bridge_query_planner) = self.js_planners.first().cloned() { - let v8_heap_used = self.v8_heap_used.clone(); - let v8_heap_total = self.v8_heap_total.clone(); - Some(Self::get_v8_metrics( bridge_query_planner, - v8_heap_used, - v8_heap_total, + self.v8_heap_used.clone(), + self.v8_heap_total.clone(), )) } else { None @@ -296,6 +305,7 @@ mod tests { ) .await .unwrap(); + pool.activate(); let query = "query { me { name } }".to_string(); let doc = Query::parse_document(&query, None, &schema, &config).unwrap(); diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index 9a4aec2e77..b0a527584e 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -85,12 +85,6 @@ pub(crate) struct CachingQueryPlanner { legacy_introspection_caching: bool, } -impl CachingQueryPlanner { - pub(crate) fn activate(&self) { - self.cache.activate() - } -} - fn init_query_plan_from_redis( subgraph_schemas: &SubgraphSchemas, cache_entry: &mut Result>, @@ -399,6 +393,11 @@ impl CachingQueryPlanner { ) -> Arc>>> { self.delegate.subgraph_schemas() } + + pub(crate) fn activate(&self) { + self.cache.activate(); + self.delegate.activate(); + } } impl tower::Service diff --git a/apollo-router/tests/integration/telemetry/metrics.rs b/apollo-router/tests/integration/telemetry/metrics.rs index f2ba74b272..cb317e2cc9 100644 --- a/apollo-router/tests/integration/telemetry/metrics.rs +++ b/apollo-router/tests/integration/telemetry/metrics.rs @@ -237,4 +237,22 @@ async fn test_gauges_on_reload() { router .assert_metrics_contains(r#"apollo_router_cache_storage_estimated_size{kind="query planner",type="memory",otel_scope_name="apollo/router"} "#, None) .await; + router + .assert_metrics_contains( + r#"apollo_router_query_planning_queued{otel_scope_name="apollo/router"} "#, + None, + ) + .await; + router + .assert_metrics_contains( + r#"apollo_router_v8_heap_total_bytes{otel_scope_name="apollo/router"} "#, + None, + ) + .await; + router + .assert_metrics_contains( + r#"apollo_router_v8_heap_total_bytes{otel_scope_name="apollo/router"} "#, + None, + ) + .await; } From e49e1eb11102f651f97194d5b9acc86b2c6ba52e Mon Sep 17 00:00:00 2001 From: Jeremy Lempereur Date: Fri, 13 Sep 2024 16:13:32 +0200 Subject: [PATCH 21/56] Type conditioned fetching in rust (#5808) This is the rust counterpart of https://github.com/apollographql/router/pull/4748 --- apollo-federation/src/operation/mod.rs | 18 +- apollo-federation/src/query_plan/display.rs | 18 +- .../src/query_plan/fetch_dependency_graph.rs | 396 +++++++++++++++++- apollo-federation/src/query_plan/mod.rs | 6 +- .../src/query_plan/query_planner.rs | 27 +- .../query_plan/query_planning_traversal.rs | 41 +- ...factory__tests__defer_is_not_buffered.snap | 2 +- .../src/plugin/test/mock/subgraph.rs | 39 +- .../src/plugins/include_subgraph_errors.rs | 4 +- .../src/query_planner/bridge_query_planner.rs | 1 + apollo-router/src/query_planner/convert.rs | 19 +- .../tests/fixtures/set_context/one.json | 142 ------- .../one_dependent_fetch_failure.json | 23 + .../set_context/one_fetch_failure.json | 46 ++ .../fixtures/set_context/one_null_param.json | 43 ++ apollo-router/tests/set_context.rs | 15 +- ...__set_context_dependent_fetch_failure.snap | 15 + ...ons___test_type_conditions_disabled-2.snap | 154 +++++++ ...ions___test_type_conditions_disabled.snap} | 4 +- ...ions___test_type_conditions_enabled-2.snap | 218 ++++++++++ ...tions___test_type_conditions_enabled.snap} | 6 +- ...ns_enabled_generate_query_fragments-2.snap | 218 ++++++++++ ...ons_enabled_generate_query_fragments.snap} | 6 +- ...ype_conditions_enabled_list_of_list-2.snap | 282 +++++++++++++ ...type_conditions_enabled_list_of_list.snap} | 6 +- ...itions_enabled_list_of_list_of_list-2.snap | 288 +++++++++++++ ...ditions_enabled_list_of_list_of_list.snap} | 6 +- ...enabled_shouldnt_make_article_fetch-2.snap | 193 +++++++++ ..._enabled_shouldnt_make_article_fetch.snap} | 6 +- apollo-router/tests/type_conditions.rs | 117 +++++- 30 files changed, 2134 insertions(+), 225 deletions(-) create mode 100644 apollo-router/tests/fixtures/set_context/one_dependent_fetch_failure.json create mode 100644 apollo-router/tests/fixtures/set_context/one_fetch_failure.json create mode 100644 apollo-router/tests/fixtures/set_context/one_null_param.json create mode 100644 apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap rename apollo-router/tests/snapshots/{type_conditions__type_conditions_disabled.snap => type_conditions___test_type_conditions_disabled.snap} (90%) create mode 100644 apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap rename apollo-router/tests/snapshots/{type_conditions__type_conditions_enabled.snap => type_conditions___test_type_conditions_enabled.snap} (91%) create mode 100644 apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap rename apollo-router/tests/snapshots/{type_conditions__type_conditions_enabled_generate_query_fragments.snap => type_conditions___test_type_conditions_enabled_generate_query_fragments.snap} (87%) create mode 100644 apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap rename apollo-router/tests/snapshots/{type_conditions__type_conditions_enabled_list_of_list.snap => type_conditions___test_type_conditions_enabled_list_of_list.snap} (92%) create mode 100644 apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap rename apollo-router/tests/snapshots/{type_conditions__type_conditions_enabled_list_of_list_of_list.snap => type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap} (92%) create mode 100644 apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap rename apollo-router/tests/snapshots/{type_conditions__type_conditions_enabled_shouldnt_make_article_fetch.snap => type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch.snap} (90%) diff --git a/apollo-federation/src/operation/mod.rs b/apollo-federation/src/operation/mod.rs index 4c17083c3b..fdbfff205b 100644 --- a/apollo-federation/src/operation/mod.rs +++ b/apollo-federation/src/operation/mod.rs @@ -1324,7 +1324,7 @@ mod field_selection { } pub(crate) fn as_path_element(&self) -> FetchDataPathElement { - FetchDataPathElement::Key(self.response_name()) + FetchDataPathElement::Key(self.response_name(), Default::default()) } } @@ -2772,7 +2772,7 @@ impl SelectionSet { response_name, alias, }| { - path.push(FetchDataPathElement::Key(alias)); + path.push(FetchDataPathElement::Key(alias, Default::default())); Arc::new(FetchDataRewrite::KeyRenamer(FetchDataKeyRenamer { path, rename_key_to: response_name, @@ -2801,7 +2801,7 @@ impl SelectionSet { remaining.push(alias); } else { at_current_level.insert( - FetchDataPathElement::Key(alias.response_name.clone()), + FetchDataPathElement::Key(alias.response_name.clone(), Default::default()), alias, ); } @@ -3110,7 +3110,10 @@ fn compute_aliases_for_non_merging_fields( Some(s) => { let mut selections = s.clone(); let mut p = path.clone(); - p.push(FetchDataPathElement::Key(response_name.clone())); + p.push(FetchDataPathElement::Key( + response_name.clone(), + Default::default(), + )); selections.push(SelectionSetAtPath { path: p, selections: field.selection_set.clone(), @@ -3136,7 +3139,7 @@ fn compute_aliases_for_non_merging_fields( let selections = match field.selection_set.as_ref() { Some(s) => { let mut p = path.clone(); - p.push(FetchDataPathElement::Key(alias.clone())); + p.push(FetchDataPathElement::Key(alias.clone(), Default::default())); Some(vec![SelectionSetAtPath { path: p, selections: Some(s.clone()), @@ -3167,7 +3170,10 @@ fn compute_aliases_for_non_merging_fields( let selections: Option> = match field.selection_set.as_ref() { Some(s) => { - path.push(FetchDataPathElement::Key(response_name.clone())); + path.push(FetchDataPathElement::Key( + response_name.clone(), + Default::default(), + )); Some(vec![SelectionSetAtPath { path, selections: Some(s.clone()), diff --git a/apollo-federation/src/query_plan/display.rs b/apollo-federation/src/query_plan/display.rs index 9141b87747..a00efef669 100644 --- a/apollo-federation/src/query_plan/display.rs +++ b/apollo-federation/src/query_plan/display.rs @@ -356,13 +356,27 @@ fn write_selections( impl fmt::Display for FetchDataPathElement { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Key(name) => f.write_str(name), - Self::AnyIndex => f.write_str("@"), + Self::Key(name, conditions) => { + f.write_str(name)?; + write_conditions(conditions, f) + } + Self::AnyIndex(conditions) => { + f.write_str("@")?; + write_conditions(conditions, f) + } Self::TypenameEquals(name) => write!(f, "... on {name}"), } } } +fn write_conditions(conditions: &[Name], f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !conditions.is_empty() { + write!(f, "|[{}]", conditions.join(",")) + } else { + Ok(()) + } +} + impl fmt::Display for QueryPathElement { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index a8368f6f74..17767c1668 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -68,6 +68,7 @@ use crate::schema::position::CompositeTypeDefinitionPosition; use crate::schema::position::FieldDefinitionPosition; use crate::schema::position::ObjectTypeDefinitionPosition; use crate::schema::position::OutputTypeDefinitionPosition; +use crate::schema::position::PositionLookupError; use crate::schema::position::SchemaRootDefinitionKind; use crate::schema::position::TypeDefinitionPosition; use crate::schema::ValidFederationSchema; @@ -203,7 +204,7 @@ type FetchDependencyGraphPetgraph = pub(crate) struct FetchDependencyGraph { /// The supergraph schema that generated the federated query graph. #[serde(skip)] - supergraph_schema: ValidFederationSchema, + pub(crate) supergraph_schema: ValidFederationSchema, /// The federated query graph that generated the fetches. (This also contains the subgraph /// schemas.) #[serde(skip)] @@ -251,11 +252,15 @@ pub(crate) struct DeferredInfo { } // TODO: Write docstrings -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub(crate) struct FetchDependencyGraphNodePath { + schema: ValidFederationSchema, pub(crate) full_path: Arc, path_in_node: Arc, response_path: Vec, + type_conditioned_fetching_enabled: bool, + possible_types: IndexSet, + possible_types_after_last_field: IndexSet, } #[derive(Debug, Clone)] @@ -443,11 +448,40 @@ impl ProcessingState { } impl FetchDependencyGraphNodePath { + pub(crate) fn new( + schema: ValidFederationSchema, + type_conditioned_fetching_enabled: bool, + root_type: CompositeTypeDefinitionPosition, + ) -> Result { + let root_possible_types = if type_conditioned_fetching_enabled { + schema.possible_runtime_types(root_type)? + } else { + Default::default() + } + .into_iter() + .map(|pos| Ok(pos.get(schema.schema())?.name.clone())) + .collect::, _>>() + .map_err(|e: PositionLookupError| FederationError::from(e))?; + + Ok(Self { + schema, + type_conditioned_fetching_enabled, + full_path: Default::default(), + path_in_node: Default::default(), + response_path: Default::default(), + possible_types: root_possible_types.clone(), + possible_types_after_last_field: root_possible_types, + }) + } fn for_new_key_fetch(&self, new_context: Arc) -> Self { Self { + schema: self.schema.clone(), full_path: self.full_path.clone(), path_in_node: new_context, response_path: self.response_path.clone(), + type_conditioned_fetching_enabled: self.type_conditioned_fetching_enabled, + possible_types: self.possible_types.clone(), + possible_types_after_last_field: self.possible_types_after_last_field.clone(), } } @@ -455,33 +489,146 @@ impl FetchDependencyGraphNodePath { &self, element: Arc, ) -> Result { + let response_path = self.updated_response_path(&element)?; + let new_possible_types = self.new_possible_types(&element)?; + let possible_types_after_last_field = if let &OpPathElement::Field(_) = element.as_ref() { + new_possible_types.clone() + } else { + self.possible_types_after_last_field.clone() + }; + Ok(Self { - response_path: self.updated_response_path(&element)?, + schema: self.schema.clone(), + response_path, full_path: Arc::new(self.full_path.with_pushed(element.clone())), path_in_node: Arc::new(self.path_in_node.with_pushed(element)), + type_conditioned_fetching_enabled: self.type_conditioned_fetching_enabled, + possible_types: new_possible_types, + possible_types_after_last_field, }) } + fn new_possible_types( + &self, + element: &OpPathElement, + ) -> Result, FederationError> { + if !self.type_conditioned_fetching_enabled { + return Ok(Default::default()); + } + + let res = match element { + OpPathElement::InlineFragment(f) => match &f.type_condition_position { + None => self.possible_types.clone(), + Some(tcp) => { + let element_possible_types = self.schema.possible_runtime_types(tcp.clone())?; + element_possible_types + .iter() + .filter(|&possible_type| { + self.possible_types.contains(&possible_type.type_name) + }) + .map(|possible_type| possible_type.type_name.clone()) + .collect() + } + }, + OpPathElement::Field(f) => self.advance_field_type(f)?, + }; + Ok(res) + } + + fn advance_field_type(&self, element: &Field) -> Result, FederationError> { + if !element + .data() + .output_base_type() + .map(|base_type| base_type.is_composite_type()) + .unwrap_or_default() + { + return Ok(Default::default()); + } + + let mut res = self + .possible_types + .clone() + .into_iter() + .map(|pt| { + let field = CompositeTypeDefinitionPosition::try_from(self.schema.get_type(pt)?)? + .field(element.name().clone())? + .get(self.schema.schema())?; + let typ = self + .schema + .get_type(field.ty.inner_named_type().clone())? + .try_into()?; + Ok(self + .schema + .possible_runtime_types(typ)? + .into_iter() + .map(|ctdp| ctdp.type_name) + .collect::>()) + }) + .collect::>, FederationError>>()? + .into_iter() + .flatten() + .collect::>(); + + res.sort(); + + Ok(res.into_iter().collect()) + } + fn updated_response_path( &self, element: &OpPathElement, ) -> Result, FederationError> { let mut new_path = self.response_path.clone(); - if let OpPathElement::Field(field) = element { - new_path.push(FetchDataPathElement::Key(field.response_name())); - // TODO: is there a simpler we to find a field’s type from `&Field`? - let mut type_ = &field.field_position.get(field.schema.schema())?.ty; - loop { - match type_ { - schema::Type::Named(_) | schema::Type::NonNullNamed(_) => break, - schema::Type::List(inner) | schema::Type::NonNullList(inner) => { - new_path.push(FetchDataPathElement::AnyIndex); - type_ = inner + match element { + OpPathElement::InlineFragment(_) => Ok(new_path), + OpPathElement::Field(field) => { + // Type conditions on the last element of a path don't imply different subgraph fetches. + // They would only translate to a potentially new fragment. + // So instead of applying a type condition to the last element of a path + // We keep track of type conditions and apply them to the parent if applicable. + // EG: + // foo.bar|[baz, qux] # |[baz, qux] aren't necessary + // foo.bar|[baz, qux].quux # |[baz, qux] apply to the parents, they are necessary + if self.possible_types_after_last_field.len() != self.possible_types.len() { + let conditions = &self.possible_types; + + match new_path.pop() { + Some(FetchDataPathElement::AnyIndex(_)) => { + new_path.push(FetchDataPathElement::AnyIndex( + conditions.iter().cloned().collect(), + )); + } + Some(FetchDataPathElement::Key(name, _)) => { + new_path.push(FetchDataPathElement::Key( + name, + conditions.iter().cloned().collect(), + )); + } + Some(other) => new_path.push(other), + None => {} + } + } + + new_path.push(FetchDataPathElement::Key( + field.response_name(), + Default::default(), + )); + + // TODO: is there a simpler way to find a field’s type from `&Field`? + let mut type_ = &field.field_position.get(field.schema.schema())?.ty; + loop { + match type_ { + schema::Type::Named(_) | schema::Type::NonNullNamed(_) => break, + schema::Type::List(inner) | schema::Type::NonNullList(inner) => { + new_path.push(FetchDataPathElement::AnyIndex(Default::default())); + type_ = inner + } } } + + Ok(new_path) } - }; - Ok(new_path) + } } } @@ -3833,7 +3980,7 @@ fn compute_input_rewrites_on_key_fetch( { // rewrite path: [ ... on , __typename ] let type_cond = FetchDataPathElement::TypenameEquals(input_type_name.clone()); - let typename_field_elem = FetchDataPathElement::Key(TYPENAME_FIELD); + let typename_field_elem = FetchDataPathElement::Key(TYPENAME_FIELD, Default::default()); let rewrite = FetchDataRewrite::ValueSetter(FetchDataValueSetter { path: vec![type_cond, typename_field_elem], set_value_to: dest_type.type_name().to_string().into(), @@ -4450,8 +4597,225 @@ fn path_for_parent( let filtered_path = path.path_in_node.filter_on_schema(parent_schema); let final_path = concat_op_paths(parent_path.deref(), &filtered_path); Ok(FetchDependencyGraphNodePath { + schema: dependency_graph.supergraph_schema.clone(), full_path: path.full_path.clone(), path_in_node: Arc::new(final_path), response_path: path.response_path.clone(), + possible_types: path.possible_types.clone(), + possible_types_after_last_field: path.possible_types_after_last_field.clone(), + type_conditioned_fetching_enabled: path.type_conditioned_fetching_enabled, }) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::schema::position::InterfaceTypeDefinitionPosition; + + #[test] + fn type_condition_fetching_disabled() { + let schema = apollo_compiler::Schema::parse_and_validate( + r#" + type Query { + foo: Foo + } + interface Foo { + bar: Bar + } + interface Bar { + baz: String + } + type Foo_1 implements Foo { + bar: Bar_1 + a: Int + } + type Foo_2 implements Foo { + bar: Bar_2 + b: Int + } + type Bar_1 implements Bar { + baz: String + a: Int + } + type Bar_2 implements Bar { + baz: String + b: Int + } + type Bar_3 implements Bar { + baz: String + } + "#, + "schema.graphql", + ) + .unwrap(); + + let valid_schema = ValidFederationSchema::new(schema.clone()).unwrap(); + + let foo = object_field_element(&valid_schema, name!("Query"), name!("foo")); + let frag = inline_fragment_element(&valid_schema, name!("Foo"), Some(name!("Foo_1"))); + let bar = object_field_element(&valid_schema, name!("Foo_1"), name!("bar")); + let frag2 = inline_fragment_element(&valid_schema, name!("Bar"), Some(name!("Bar_1"))); + let baz = object_field_element(&valid_schema, name!("Bar_1"), name!("baz")); + + let query_root = valid_schema + .get_type(name!("Query")) + .unwrap() + .try_into() + .unwrap(); + + let path = FetchDependencyGraphNodePath::new(valid_schema, false, query_root).unwrap(); + + let path = path.add(Arc::new(foo)).unwrap(); + let path = path.add(Arc::new(frag)).unwrap(); + let path = path.add(Arc::new(bar)).unwrap(); + let path = path.add(Arc::new(frag2)).unwrap(); + let path = path.add(Arc::new(baz)).unwrap(); + + assert_eq!(".foo.bar.baz", &to_string(&path.response_path)); + } + + #[test] + fn type_condition_fetching_enabled() { + let schema = apollo_compiler::Schema::parse_and_validate( + r#" + type Query { + foo: Foo + } + interface Foo { + bar: Bar + } + interface Bar { + baz: String + } + type Foo_1 implements Foo { + bar: Bar_1 + a: Int + } + type Foo_2 implements Foo { + bar: Bar_2 + b: Int + } + type Bar_1 implements Bar { + baz: String + a: Int + } + type Bar_2 implements Bar { + baz: String + b: Int + } + type Bar_3 implements Bar { + baz: String + } + "#, + "schema.graphql", + ) + .unwrap(); + + let valid_schema = ValidFederationSchema::new(schema.clone()).unwrap(); + + let foo = object_field_element(&valid_schema, name!("Query"), name!("foo")); + let frag = inline_fragment_element(&valid_schema, name!("Foo"), Some(name!("Foo_1"))); + let bar = object_field_element(&valid_schema, name!("Foo_1"), name!("bar")); + let frag2 = inline_fragment_element(&valid_schema, name!("Bar"), Some(name!("Bar_1"))); + let baz = object_field_element(&valid_schema, name!("Bar_1"), name!("baz")); + + let query_root = valid_schema + .get_type(name!("Query")) + .unwrap() + .try_into() + .unwrap(); + + let path = FetchDependencyGraphNodePath::new(valid_schema, true, query_root).unwrap(); + + let path = path.add(Arc::new(foo)).unwrap(); + let path = path.add(Arc::new(frag)).unwrap(); + let path = path.add(Arc::new(bar)).unwrap(); + let path = path.add(Arc::new(frag2)).unwrap(); + let path = path.add(Arc::new(baz)).unwrap(); + + assert_eq!(".|[Foo_1]foo.bar.baz", &to_string(&path.response_path)); + } + + fn object_field_element( + schema: &ValidFederationSchema, + object: apollo_compiler::Name, + field: apollo_compiler::Name, + ) -> OpPathElement { + OpPathElement::Field(super::Field::new(super::FieldData { + schema: schema.clone(), + field_position: ObjectTypeDefinitionPosition::new(object) + .field(field) + .into(), + alias: None, + arguments: Default::default(), + directives: Default::default(), + sibling_typename: None, + })) + } + + fn interface_field_element( + schema: &ValidFederationSchema, + interface: apollo_compiler::Name, + field: apollo_compiler::Name, + ) -> OpPathElement { + OpPathElement::Field(super::Field::new(super::FieldData { + schema: schema.clone(), + field_position: InterfaceTypeDefinitionPosition::new(interface) + .field(field) + .into(), + alias: None, + arguments: Default::default(), + directives: Default::default(), + sibling_typename: None, + })) + } + + fn inline_fragment_element( + schema: &ValidFederationSchema, + parent_type_name: apollo_compiler::Name, + type_condition_name: Option, + ) -> OpPathElement { + let parent_type = schema + .get_type(parent_type_name) + .unwrap() + .try_into() + .unwrap(); + let type_condition = + type_condition_name.map(|n| schema.get_type(n).unwrap().try_into().unwrap()); + OpPathElement::InlineFragment(super::InlineFragment::new(InlineFragmentData { + schema: schema.clone(), + parent_type_position: parent_type, + type_condition_position: type_condition, + directives: Default::default(), + selection_id: SelectionId::new(), + })) + } + + fn to_string(response_path: &[FetchDataPathElement]) -> String { + format!( + ".{}", + response_path + .iter() + .map(|element| match element { + FetchDataPathElement::Key(name, conditions) => { + format!("{}{}", cond_to_string(conditions), name) + } + FetchDataPathElement::AnyIndex(conditions) => { + format!("{}{}", cond_to_string(conditions), "@") + } + FetchDataPathElement::TypenameEquals(_) => { + unimplemented!() + } + }) + .join(".") + ) + } + + fn cond_to_string(conditions: &[Name]) -> String { + if conditions.is_empty() { + return Default::default(); + } + + format!("|[{}]", conditions.iter().map(|n| n.to_string()).join(",")) + } +} diff --git a/apollo-federation/src/query_plan/mod.rs b/apollo-federation/src/query_plan/mod.rs index f620e208b3..d8c90c2c4a 100644 --- a/apollo-federation/src/query_plan/mod.rs +++ b/apollo-federation/src/query_plan/mod.rs @@ -235,11 +235,13 @@ pub struct FetchDataKeyRenamer { /// elements. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] pub enum FetchDataPathElement { - Key(Name), - AnyIndex, + Key(Name, Conditions), + AnyIndex(Conditions), TypenameEquals(Name), } +pub type Conditions = Vec; + /// Vectors of this element match a path in a query. Each element is (1) a field in a query, or (2) /// an inline fragment in a query. #[derive(Debug, Clone, PartialEq, serde::Serialize)] diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index ec338ef59d..c2c2d75805 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -24,6 +24,7 @@ use crate::query_graph::QueryGraph; use crate::query_graph::QueryGraphNodeType; use crate::query_plan::fetch_dependency_graph::compute_nodes_for_tree; use crate::query_plan::fetch_dependency_graph::FetchDependencyGraph; +use crate::query_plan::fetch_dependency_graph::FetchDependencyGraphNodePath; use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphProcessor; use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphToCostProcessor; use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphToQueryPlanProcessor; @@ -95,6 +96,14 @@ pub struct QueryPlannerConfig { /// in this sub-set are provided without guarantees of stability (they may be dangerous) or /// continued support (they may be removed without warning). pub debug: QueryPlannerDebugConfig, + + /// Enables type conditioned fetching. + /// This flag is a workaround, which may yield significant + /// performance degradation when computing query plans, + /// and increase query plan size. + /// + /// If you aren't aware of this flag, you probably don't need it. + pub type_conditioned_fetching: bool, } impl Default for QueryPlannerConfig { @@ -105,6 +114,7 @@ impl Default for QueryPlannerConfig { generate_query_fragments: false, incremental_delivery: Default::default(), debug: Default::default(), + type_conditioned_fetching: Default::default(), } } } @@ -682,6 +692,7 @@ fn compute_root_serial_dependency_graph( operation.root_kind, &mut fetch_dependency_graph, &prev_path, + parameters.config.type_conditioned_fetching, )?; } else { // PORT_NOTE: It is unclear if they correct thing to do here is get the next ID, use @@ -719,6 +730,7 @@ pub(crate) fn compute_root_fetch_groups( root_kind: SchemaRootDefinitionKind, dependency_graph: &mut FetchDependencyGraph, path: &OpPathTree, + type_conditioned_fetching_enabled: bool, ) -> Result<(), FederationError> { // The root of the pathTree is one of the "fake" root of the subgraphs graph, // which belongs to no subgraph but points to each ones. @@ -731,7 +743,7 @@ pub(crate) fn compute_root_fetch_groups( let (_source_node, target_node) = path.graph.edge_endpoints(edge)?; let target_node = path.graph.node_weight(target_node)?; let subgraph_name = &target_node.source; - let root_type = match &target_node.type_ { + let root_type: CompositeTypeDefinitionPosition = match &target_node.type_ { QueryGraphNodeType::SchemaType(OutputTypeDefinitionPosition::Object(object)) => { object.clone().into() } @@ -741,14 +753,21 @@ pub(crate) fn compute_root_fetch_groups( ))) } }; - let fetch_dependency_node = - dependency_graph.get_or_create_root_node(subgraph_name, root_kind, root_type)?; + let fetch_dependency_node = dependency_graph.get_or_create_root_node( + subgraph_name, + root_kind, + root_type.clone(), + )?; snapshot!(dependency_graph, "tree_with_root_node"); compute_nodes_for_tree( dependency_graph, &child.tree, fetch_dependency_node, - Default::default(), + FetchDependencyGraphNodePath::new( + dependency_graph.supergraph_schema.clone(), + type_conditioned_fetching_enabled, + root_type, + )?, Default::default(), &Default::default(), )?; diff --git a/apollo-federation/src/query_plan/query_planning_traversal.rs b/apollo-federation/src/query_plan/query_planning_traversal.rs index 2dab41debf..c54a81ba66 100644 --- a/apollo-federation/src/query_plan/query_planning_traversal.rs +++ b/apollo-federation/src/query_plan/query_planning_traversal.rs @@ -30,6 +30,7 @@ use crate::query_graph::QueryGraph; use crate::query_graph::QueryGraphNodeType; use crate::query_plan::fetch_dependency_graph::compute_nodes_for_tree; use crate::query_plan::fetch_dependency_graph::FetchDependencyGraph; +use crate::query_plan::fetch_dependency_graph::FetchDependencyGraphNodePath; use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphProcessor; use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphToCostProcessor; use crate::query_plan::generate::generate_all_plans_and_find_best; @@ -663,7 +664,11 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { *root, &single_choice_branches, )?; - self.updated_dependency_graph(&mut initial_dependency_graph, &initial_tree)?; + self.updated_dependency_graph( + &mut initial_dependency_graph, + &initial_tree, + self.parameters.config.type_conditioned_fetching, + )?; snapshot!( initial_dependency_graph, "Updated dep graph with initial tree" @@ -963,34 +968,44 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { &self, dependency_graph: &mut FetchDependencyGraph, path_tree: &OpPathTree, + type_conditioned_fetching_enabled: bool, ) -> Result<(), FederationError> { let is_root_path_tree = matches!( path_tree.graph.node_weight(path_tree.node)?.type_, QueryGraphNodeType::FederatedRootType(_) ); if is_root_path_tree { - compute_root_fetch_groups(self.root_kind, dependency_graph, path_tree)?; + compute_root_fetch_groups( + self.root_kind, + dependency_graph, + path_tree, + type_conditioned_fetching_enabled, + )?; } else { let query_graph_node = path_tree.graph.node_weight(path_tree.node)?; let subgraph_name = &query_graph_node.source; - let root_type = match &query_graph_node.type_ { + let root_type: CompositeTypeDefinitionPosition = match &query_graph_node.type_ { QueryGraphNodeType::SchemaType(position) => position.clone().try_into()?, QueryGraphNodeType::FederatedRootType(_) => { return Err(FederationError::internal( "unexpected FederatedRootType not at the start of an OpPathTree", - )) + )); } }; let fetch_dependency_node = dependency_graph.get_or_create_root_node( subgraph_name, self.root_kind, - root_type, + root_type.clone(), )?; compute_nodes_for_tree( dependency_graph, path_tree, fetch_dependency_node, - Default::default(), + FetchDependencyGraphNodePath::new( + dependency_graph.supergraph_schema.clone(), + self.parameters.config.type_conditioned_fetching, + root_type, + )?, Default::default(), &Default::default(), )?; @@ -1069,11 +1084,15 @@ impl<'a: 'b, 'b> PlanBuilder> for QueryPlanningTravers tree: Arc, ) -> Result { let mut updated_graph = plan_info.fetch_dependency_graph.clone(); - self.updated_dependency_graph(&mut updated_graph, &tree) - .map(|_| PlanInfo { - fetch_dependency_graph: updated_graph, - path_tree: plan_info.path_tree.merge(&tree), - }) + self.updated_dependency_graph( + &mut updated_graph, + &tree, + self.parameters.config.type_conditioned_fetching, + ) + .map(|_| PlanInfo { + fetch_dependency_graph: updated_graph, + path_tree: plan_info.path_tree.merge(&tree), + }) } fn compute_plan_cost( diff --git a/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap b/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap index 6d6e785101..c08d3d1693 100644 --- a/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap +++ b/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap @@ -20,7 +20,7 @@ expression: parts }, "errors": [ { - "message": "couldn't find mock for query {\"query\":\"query TopProducts__reviews__1($representations:[_Any!]!){_entities(representations:$representations){...on Product{reviews{__typename id product{__typename upc}}}}}\",\"operationName\":\"TopProducts__reviews__1\",\"variables\":{\"representations\":[{\"__typename\":\"Product\",\"upc\":\"1\"},{\"__typename\":\"Product\",\"upc\":\"2\"}]}}", + "message": "couldn't find mock for query {\"query\":\"query($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { reviews { __typename id product { __typename upc } } } } }\",\"variables\":{\"representations\":[{\"__typename\":\"Product\",\"upc\":\"1\"},{\"__typename\":\"Product\",\"upc\":\"2\"}]}}", "path": [ "topProducts", "@" diff --git a/apollo-router/src/plugin/test/mock/subgraph.rs b/apollo-router/src/plugin/test/mock/subgraph.rs index 8ee5e5465c..46321b585b 100644 --- a/apollo-router/src/plugin/test/mock/subgraph.rs +++ b/apollo-router/src/plugin/test/mock/subgraph.rs @@ -6,6 +6,8 @@ use std::collections::HashMap; use std::sync::Arc; use std::task::Poll; +use apollo_compiler::ast::Definition; +use apollo_compiler::ast::Document; use futures::future; use http::HeaderMap; use http::HeaderName; @@ -38,7 +40,15 @@ pub struct MockSubgraph { impl MockSubgraph { pub fn new(mocks: MockResponses) -> Self { Self { - mocks: Arc::new(mocks), + mocks: Arc::new( + mocks + .into_iter() + .map(|(mut req, res)| { + normalize(&mut req); + (req, res) + }) + .collect(), + ), extensions: None, subscription_stream: None, map_request_fn: None, @@ -91,11 +101,10 @@ impl MockSubgraphBuilder { /// /// the arguments must deserialize to `crate::graphql::Request` and `crate::graphql::Response` pub fn with_json(mut self, request: serde_json::Value, response: serde_json::Value) -> Self { - self.mocks.insert( - serde_json::from_value(request).unwrap(), - serde_json::from_value(response).unwrap(), - ); - + let mut request = serde_json::from_value(request).unwrap(); + normalize(&mut request); + self.mocks + .insert(request, serde_json::from_value(response).unwrap()); self } @@ -123,6 +132,22 @@ impl MockSubgraphBuilder { } } +// Normalize queries so that spaces and operation names +// don't have an impact on the cache +fn normalize(request: &mut Request) { + if let Some(q) = &request.query { + let mut doc = Document::parse(q.clone(), "request").unwrap(); + + if let Some(Definition::OperationDefinition(ref mut op)) = doc.definitions.first_mut() { + let o = op.make_mut(); + o.name.take(); + }; + + request.query = Some(doc.serialize().no_indent().to_string()); + request.operation_name = None; + } +} + impl Service for MockSubgraph { type Response = SubgraphResponse; @@ -173,6 +198,8 @@ impl Service for MockSubgraph { } } + normalize(body); + let response = if let Some(response) = self.mocks.get(body) { // Build an http Response let mut http_response_builder = http::Response::builder().status(StatusCode::OK); diff --git a/apollo-router/src/plugins/include_subgraph_errors.rs b/apollo-router/src/plugins/include_subgraph_errors.rs index 5f6eb355d1..34348f4ef7 100644 --- a/apollo-router/src/plugins/include_subgraph_errors.rs +++ b/apollo-router/src/plugins/include_subgraph_errors.rs @@ -104,7 +104,7 @@ mod test { use crate::Configuration; static UNREDACTED_PRODUCT_RESPONSE: Lazy = Lazy::new(|| { - Bytes::from_static(r#"{"data":{"topProducts":null},"errors":[{"message":"couldn't find mock for query {\"query\":\"query ErrorTopProducts__products__0($first:Int){topProducts(first:$first){__typename upc name}}\",\"operationName\":\"ErrorTopProducts__products__0\",\"variables\":{\"first\":2}}","path":[],"extensions":{"test":"value","code":"FETCH_ERROR"}}]}"#.as_bytes()) + Bytes::from_static(r#"{"data":{"topProducts":null},"errors":[{"message":"couldn't find mock for query {\"query\":\"query($first: Int) { topProducts(first: $first) { __typename upc } }\",\"variables\":{\"first\":2}}","path":[],"extensions":{"test":"value","code":"FETCH_ERROR"}}]}"#.as_bytes()) }); static REDACTED_PRODUCT_RESPONSE: Lazy = Lazy::new(|| { @@ -127,7 +127,7 @@ mod test { static VALID_QUERY: &str = r#"query TopProducts($first: Int) { topProducts(first: $first) { upc name reviews { id product { name } author { id name } } } }"#; - static ERROR_PRODUCT_QUERY: &str = r#"query ErrorTopProducts($first: Int) { topProducts(first: $first) { upc name reviews { id product { name } author { id name } } } }"#; + static ERROR_PRODUCT_QUERY: &str = r#"query ErrorTopProducts($first: Int) { topProducts(first: $first) { upc reviews { id product { name } author { id name } } } }"#; static ERROR_ACCOUNT_QUERY: &str = r#"query Query { me { name }}"#; diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 11dd9c2b08..8895320cc5 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -188,6 +188,7 @@ impl PlannerMode { apollo_federation::query_plan::query_planner::QueryPlanIncrementalDeliveryConfig { enable_defer: configuration.supergraph.defer_support, }, + type_conditioned_fetching: configuration.experimental_type_conditioned_fetching, debug: Default::default(), }; let result = QueryPlanner::new(schema.federation_supergraph(), config); diff --git a/apollo-router/src/query_planner/convert.rs b/apollo-router/src/query_planner/convert.rs index bf59d1861e..27592834c6 100644 --- a/apollo-router/src/query_planner/convert.rs +++ b/apollo-router/src/query_planner/convert.rs @@ -306,10 +306,23 @@ impl From<&'_ next::FetchDataKeyRenamer> for rewrites::DataKeyRenamer { impl From<&'_ next::FetchDataPathElement> for crate::json_ext::PathElement { fn from(value: &'_ next::FetchDataPathElement) -> Self { + // TODO: Go all in on Name eventually match value { - // TODO: Type conditioned fetching once it's available in the rust planner - next::FetchDataPathElement::Key(value) => Self::Key(value.to_string(), None), - next::FetchDataPathElement::AnyIndex => Self::Flatten(None), + next::FetchDataPathElement::Key(name, conditions) => Self::Key( + name.to_string(), + if conditions.is_empty() { + None + } else { + Some(conditions.iter().map(|c| c.to_string()).collect()) + }, + ), + next::FetchDataPathElement::AnyIndex(conditions) => { + Self::Flatten(if conditions.is_empty() { + None + } else { + Some(conditions.iter().map(|c| c.to_string()).collect()) + }) + } next::FetchDataPathElement::TypenameEquals(value) => Self::Fragment(value.to_string()), } } diff --git a/apollo-router/tests/fixtures/set_context/one.json b/apollo-router/tests/fixtures/set_context/one.json index e552a0f47b..cde9637ea5 100644 --- a/apollo-router/tests/fixtures/set_context/one.json +++ b/apollo-router/tests/fixtures/set_context/one.json @@ -19,25 +19,6 @@ } } }, - { - "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}}}", @@ -140,27 +121,6 @@ } } }, - { - "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)}}}", @@ -269,45 +229,6 @@ } } }, - { - "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}}}", @@ -346,69 +267,6 @@ ] } } - }, - { - "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/one_dependent_fetch_failure.json b/apollo-router/tests/fixtures/set_context/one_dependent_fetch_failure.json new file mode 100644 index 0000000000..9dd16bd60f --- /dev/null +++ b/apollo-router/tests/fixtures/set_context/one_dependent_fetch_failure.json @@ -0,0 +1,23 @@ +{ + "mocks": [ + { + "request": { + "query": "query Query_fetch_dependent_failure__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query_fetch_dependent_failure__Subgraph1__0" + }, + "response": { + "data": null, + "errors": [{ + "message": "Some error", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": ["t", "u"] + }] + } + } + ] +} \ No newline at end of file diff --git a/apollo-router/tests/fixtures/set_context/one_fetch_failure.json b/apollo-router/tests/fixtures/set_context/one_fetch_failure.json new file mode 100644 index 0000000000..5515102f2d --- /dev/null +++ b/apollo-router/tests/fixtures/set_context/one_fetch_failure.json @@ -0,0 +1,46 @@ +{ + "mocks": [ + { + "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" + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/apollo-router/tests/fixtures/set_context/one_null_param.json b/apollo-router/tests/fixtures/set_context/one_null_param.json new file mode 100644 index 0000000000..f36994a1a0 --- /dev/null +++ b/apollo-router/tests/fixtures/set_context/one_null_param.json @@ -0,0 +1,43 @@ +{ + "mocks": [ + { + "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 + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/apollo-router/tests/set_context.rs b/apollo-router/tests/set_context.rs index bc5f44d2da..47d8deb076 100644 --- a/apollo-router/tests/set_context.rs +++ b/apollo-router/tests/set_context.rs @@ -206,7 +206,10 @@ async fn test_set_context_with_null() { let response = run_single_request( QUERY, &[ - ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ( + "Subgraph1", + include_str!("fixtures/set_context/one_null_param.json"), + ), ("Subgraph2", include_str!("fixtures/set_context/two.json")), ], ) @@ -259,7 +262,10 @@ async fn test_set_context_unrelated_fetch_failure() { let response = run_single_request( QUERY, &[ - ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ( + "Subgraph1", + include_str!("fixtures/set_context/one_fetch_failure.json"), + ), ("Subgraph2", include_str!("fixtures/set_context/two.json")), ], ) @@ -285,7 +291,10 @@ async fn test_set_context_dependent_fetch_failure() { let response = run_single_request( QUERY, &[ - ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ( + "Subgraph1", + include_str!("fixtures/set_context/one_dependent_fetch_failure.json"), + ), ("Subgraph2", include_str!("fixtures/set_context/two.json")), ], ) 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 index 099d36a7cb..4c44587cdd 100644 --- 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 @@ -4,6 +4,21 @@ expression: response --- { "data": null, + "errors": [ + { + "message": "Some error", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": [ + "t", + "u" + ] + } + ], "extensions": { "apolloQueryPlan": { "object": { diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap new file mode 100644 index 0000000000..17f0ec285c --- /dev/null +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap @@ -0,0 +1,154 @@ +--- +source: apollo-router/tests/type_conditions.rs +expression: response +--- +{ + "data": { + "search": [ + { + "id": "a7052397-b605-414a-aba4-408d51c8eef0", + "sections": [ + { + "artwork": "Hello World", + "title": "d0182b8a-a671-4244-ba1c-905274b0d198 title" + }, + { + "artwork": "Hello World", + "title": "e6eec2fc-05ce-40a2-956b-f1335e615204 title" + } + ] + }, + { + "id": "3a7b08c9-d8c0-4c55-b55d-596a272392e0", + "sections": [ + { + "artwork": "Hello World", + "title": "f44f584e-5d3d-4466-96f5-9afc3f5d5a54 title" + }, + { + "artwork": "Hello World" + } + ] + }, + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "Hello World" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "Hello World" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + }, + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "Hello World" + }, + { + "artwork": "Hello World", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ] + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "searchSubgraph", + "variableUsages": [], + "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operationName": "Search__searchSubgraph__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "587c887350ef75eaf4b647be94fd682616bcd33909e15fb797cee226e95fa36a", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + }, + { + "kind": "Flatten", + "path": [ + "search", + "@", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "movieResultParam" + ], + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operationName": "Search__artworkSubgraph__1", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "622eb49d4e52ff2636348e103f941d04b783fec97de59d0ae6635d9272f97ad8", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Flatten(path: \"search.@.sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\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___test_type_conditions_disabled.snap similarity index 90% rename from apollo-router/tests/snapshots/type_conditions__type_conditions_disabled.snap rename to apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap index 224cd2fb09..f355685192 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_disabled.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap @@ -72,7 +72,7 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0{search{__typename ...on MovieResult{sections{__typename ...on EntityCollectionSection{__typename id}...on GallerySection{__typename id}}id}...on ArticleResult{id sections{__typename ...on GallerySection{__typename id}...on EntityCollectionSection{__typename id}}}}}", + "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, @@ -130,7 +130,7 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){...on EntityCollectionSection{title artwork(params:$movieResultParam)}...on GallerySection{artwork(params:$movieResultParam)}}}", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap new file mode 100644 index 0000000000..01e49a0721 --- /dev/null +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap @@ -0,0 +1,218 @@ +--- +source: apollo-router/tests/type_conditions.rs +expression: response +--- +{ + "data": { + "search": [ + { + "id": "a7052397-b605-414a-aba4-408d51c8eef0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "d0182b8a-a671-4244-ba1c-905274b0d198 title" + }, + { + "artwork": "articleResultEnabled artwork", + "title": "e6eec2fc-05ce-40a2-956b-f1335e615204 title" + } + ] + }, + { + "id": "3a7b08c9-d8c0-4c55-b55d-596a272392e0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "f44f584e-5d3d-4466-96f5-9afc3f5d5a54 title" + }, + { + "artwork": "articleResultEnabled artwork" + } + ] + }, + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "movieResultEnabled artwork" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "movieResultEnabled artwork" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + }, + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "movieResultEnabled artwork" + }, + { + "artwork": "movieResultEnabled artwork", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ] + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "searchSubgraph", + "variableUsages": [], + "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operationName": "Search__searchSubgraph__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "587c887350ef75eaf4b647be94fd682616bcd33909e15fb797cee226e95fa36a", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "path": [ + "search", + "@|[ArticleResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "articleResultParam" + ], + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", + "operationName": "Search__artworkSubgraph__1", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "a0bf36d3a611df53c3a60b9b124a2887f2d266858221c606ace0985d101d64bd", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + }, + { + "kind": "Flatten", + "path": [ + "search", + "@|[MovieResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "movieResultParam" + ], + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operationName": "Search__artworkSubgraph__2", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "3e84a53f967bf40d4c08254a94f3fa32a828ab3ad8184a22bb3439c596ecaaf4", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \"search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n Flatten(path: \"search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap similarity index 91% rename from apollo-router/tests/snapshots/type_conditions__type_conditions_enabled.snap rename to apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap index da66cee5c2..a8afd1fa0e 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap @@ -72,7 +72,7 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0{search{__typename ...on MovieResult{sections{__typename ...on EntityCollectionSection{__typename id}...on GallerySection{__typename id}}id}...on ArticleResult{id sections{__typename ...on GallerySection{__typename id}...on EntityCollectionSection{__typename id}}}}}", + "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, @@ -134,7 +134,7 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){...on EntityCollectionSection{title artwork(params:$movieResultParam)}...on GallerySection{artwork(params:$movieResultParam)}}}", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, @@ -194,7 +194,7 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations:[_Any!]!$articleResultParam:String){_entities(representations:$representations){...on GallerySection{artwork(params:$articleResultParam)}...on EntityCollectionSection{artwork(params:$articleResultParam)title}}}", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap new file mode 100644 index 0000000000..ed94ed7a85 --- /dev/null +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap @@ -0,0 +1,218 @@ +--- +source: apollo-router/tests/type_conditions.rs +expression: response +--- +{ + "data": { + "search": [ + { + "id": "a7052397-b605-414a-aba4-408d51c8eef0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "d0182b8a-a671-4244-ba1c-905274b0d198 title" + }, + { + "artwork": "articleResultEnabled artwork", + "title": "e6eec2fc-05ce-40a2-956b-f1335e615204 title" + } + ] + }, + { + "id": "3a7b08c9-d8c0-4c55-b55d-596a272392e0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "f44f584e-5d3d-4466-96f5-9afc3f5d5a54 title" + }, + { + "artwork": "articleResultEnabled artwork" + } + ] + }, + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "movieResultEnabled artwork" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "movieResultEnabled artwork" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + }, + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "movieResultEnabled artwork" + }, + { + "artwork": "movieResultEnabled artwork", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ] + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "searchSubgraph", + "variableUsages": [], + "operation": "query Search__searchSubgraph__0 { search { __typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { __typename id } fragment _generated_onGallerySection2_0 on GallerySection { __typename id } fragment _generated_onMovieResult2_0 on MovieResult { sections { __typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0 } id } fragment _generated_onArticleResult2_0 on ArticleResult { id sections { __typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0 } }", + "operationName": "Search__searchSubgraph__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "70ca85b28e861b24a7749862930a5f965c4c6e8074d60a87a3952d596fe7cc36", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "path": [ + "search", + "@|[ArticleResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "articleResultParam" + ], + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", + "operationName": "Search__artworkSubgraph__1", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "1d21a65a3b5a31e17f7834750ef5b37fb49d99d0a1e2145f00a62d43c5f8423a", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + }, + { + "kind": "Flatten", + "path": [ + "search", + "@|[MovieResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "movieResultParam" + ], + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", + "operationName": "Search__artworkSubgraph__2", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "df321f6532c2c9eda0d8c042e5f08073c24e558dd0cae01054886b79416a6c08", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n\n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n\n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n\n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \"search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n Flatten(path: \"search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_generate_query_fragments.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments.snap similarity index 87% rename from apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_generate_query_fragments.snap rename to apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments.snap index e5e2cc616a..08a9782c85 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_generate_query_fragments.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments.snap @@ -72,7 +72,7 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0{search{__typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0}}fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection{__typename id}fragment _generated_onGallerySection2_0 on GallerySection{__typename id}fragment _generated_onMovieResult2_0 on MovieResult{sections{__typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0}id}fragment _generated_onArticleResult2_0 on ArticleResult{id sections{__typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0}}", + "operation": "query Search__searchSubgraph__0 { search { __typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { __typename id } fragment _generated_onGallerySection2_0 on GallerySection { __typename id } fragment _generated_onMovieResult2_0 on MovieResult { sections { __typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0 } id } fragment _generated_onArticleResult2_0 on ArticleResult { id sections { __typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0 } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, @@ -134,7 +134,7 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){..._generated_onEntityCollectionSection2_0 ...on GallerySection{artwork(params:$movieResultParam)}}}fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection{title artwork(params:$movieResultParam)}", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, @@ -194,7 +194,7 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations:[_Any!]!$articleResultParam:String){_entities(representations:$representations){...on GallerySection{artwork(params:$articleResultParam)}..._generated_onEntityCollectionSection2_0}}fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection{artwork(params:$articleResultParam)title}", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap new file mode 100644 index 0000000000..c990725153 --- /dev/null +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap @@ -0,0 +1,282 @@ +--- +source: apollo-router/tests/type_conditions.rs +expression: response +--- +{ + "data": { + "searchListOfList": [ + [ + { + "id": "a7052397-b605-414a-aba4-408d51c8eef0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "d0182b8a-a671-4244-ba1c-905274b0d198 title" + }, + { + "artwork": "articleResultEnabled artwork", + "title": "e6eec2fc-05ce-40a2-956b-f1335e615204 title" + } + ] + }, + { + "id": "3a7b08c9-d8c0-4c55-b55d-596a272392e0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "f44f584e-5d3d-4466-96f5-9afc3f5d5a54 title" + }, + { + "artwork": "articleResultEnabled artwork" + } + ] + }, + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "movieResultEnabled artwork" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "movieResultEnabled artwork" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + }, + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "movieResultEnabled artwork" + }, + { + "artwork": "movieResultEnabled artwork", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ], + [ + { + "id": "a7052397-b605-414a-aba4-408d51c8eef0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "d0182b8a-a671-4244-ba1c-905274b0d198 title" + }, + { + "artwork": "articleResultEnabled artwork", + "title": "e6eec2fc-05ce-40a2-956b-f1335e615204 title" + } + ] + }, + { + "id": "3a7b08c9-d8c0-4c55-b55d-596a272392e0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "f44f584e-5d3d-4466-96f5-9afc3f5d5a54 title" + }, + { + "artwork": "articleResultEnabled artwork" + } + ] + } + ], + [ + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "movieResultEnabled artwork" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "movieResultEnabled artwork" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + } + ], + [ + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "movieResultEnabled artwork" + }, + { + "artwork": "movieResultEnabled artwork", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ] + ] + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "searchSubgraph", + "variableUsages": [], + "operation": "query Search__searchSubgraph__0 { searchListOfList { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operationName": "Search__searchSubgraph__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "365685f6b9f1c5dd02506b27f50d63486f1ca6b5ced7b0253fc050ef73732e03", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "path": [ + "searchListOfList", + "@", + "@|[ArticleResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "articleResultParam" + ], + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", + "operationName": "Search__artworkSubgraph__1", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "a0bf36d3a611df53c3a60b9b124a2887f2d266858221c606ace0985d101d64bd", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + }, + { + "kind": "Flatten", + "path": [ + "searchListOfList", + "@", + "@|[MovieResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "movieResultParam" + ], + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operationName": "Search__artworkSubgraph__2", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "3e84a53f967bf40d4c08254a94f3fa32a828ab3ad8184a22bb3439c596ecaaf4", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfList {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \"searchListOfList.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n Flatten(path: \"searchListOfList.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap similarity index 92% rename from apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list.snap rename to apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap index 9d70336225..adf981893a 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap @@ -134,7 +134,7 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0{searchListOfList{__typename ...on MovieResult{sections{__typename ...on EntityCollectionSection{__typename id}...on GallerySection{__typename id}}id}...on ArticleResult{id sections{__typename ...on GallerySection{__typename id}...on EntityCollectionSection{__typename id}}}}}", + "operation": "query Search__searchSubgraph__0 { searchListOfList { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, @@ -197,7 +197,7 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){...on EntityCollectionSection{title artwork(params:$movieResultParam)}...on GallerySection{artwork(params:$movieResultParam)}}}", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, @@ -258,7 +258,7 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations:[_Any!]!$articleResultParam:String){_entities(representations:$representations){...on GallerySection{artwork(params:$articleResultParam)}...on EntityCollectionSection{artwork(params:$articleResultParam)title}}}", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap new file mode 100644 index 0000000000..f206e94c89 --- /dev/null +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap @@ -0,0 +1,288 @@ +--- +source: apollo-router/tests/type_conditions.rs +expression: response +--- +{ + "data": { + "searchListOfListOfList": [ + [ + [ + { + "id": "a7052397-b605-414a-aba4-408d51c8eef0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "d0182b8a-a671-4244-ba1c-905274b0d198 title" + }, + { + "artwork": "articleResultEnabled artwork", + "title": "e6eec2fc-05ce-40a2-956b-f1335e615204 title" + } + ] + }, + { + "id": "3a7b08c9-d8c0-4c55-b55d-596a272392e0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "f44f584e-5d3d-4466-96f5-9afc3f5d5a54 title" + }, + { + "artwork": "articleResultEnabled artwork" + } + ] + }, + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "movieResultEnabled artwork" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "movieResultEnabled artwork" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + }, + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "movieResultEnabled artwork" + }, + { + "artwork": "movieResultEnabled artwork", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ], + [ + { + "id": "a7052397-b605-414a-aba4-408d51c8eef0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "d0182b8a-a671-4244-ba1c-905274b0d198 title" + }, + { + "artwork": "articleResultEnabled artwork", + "title": "e6eec2fc-05ce-40a2-956b-f1335e615204 title" + } + ] + }, + { + "id": "3a7b08c9-d8c0-4c55-b55d-596a272392e0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "f44f584e-5d3d-4466-96f5-9afc3f5d5a54 title" + }, + { + "artwork": "articleResultEnabled artwork" + } + ] + } + ] + ], + [ + [ + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "movieResultEnabled artwork" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "movieResultEnabled artwork" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + } + ], + [ + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "movieResultEnabled artwork" + }, + { + "artwork": "movieResultEnabled artwork", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ] + ] + ] + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "searchSubgraph", + "variableUsages": [], + "operation": "query Search__searchSubgraph__0 { searchListOfListOfList { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operationName": "Search__searchSubgraph__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "47d7643dd7226529814a57e0382aafb3790c2d8a8b26354aa2f60e9c9f097a05", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "path": [ + "searchListOfListOfList", + "@", + "@", + "@|[ArticleResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "articleResultParam" + ], + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", + "operationName": "Search__artworkSubgraph__1", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "a0bf36d3a611df53c3a60b9b124a2887f2d266858221c606ace0985d101d64bd", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + }, + { + "kind": "Flatten", + "path": [ + "searchListOfListOfList", + "@", + "@", + "@|[MovieResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "movieResultParam" + ], + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operationName": "Search__artworkSubgraph__2", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "3e84a53f967bf40d4c08254a94f3fa32a828ab3ad8184a22bb3439c596ecaaf4", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfListOfList {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \"searchListOfListOfList.@.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n Flatten(path: \"searchListOfListOfList.@.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n },\n },\n}" + } + } +} 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___test_type_conditions_enabled_list_of_list_of_list.snap similarity index 92% rename from apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list_of_list.snap rename to apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap index 5a6a4b30bc..78a539ec02 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list_of_list.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap @@ -138,7 +138,7 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0{searchListOfListOfList{__typename ...on MovieResult{sections{__typename ...on EntityCollectionSection{__typename id}...on GallerySection{__typename id}}id}...on ArticleResult{id sections{__typename ...on GallerySection{__typename id}...on EntityCollectionSection{__typename id}}}}}", + "operation": "query Search__searchSubgraph__0 { searchListOfListOfList { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, @@ -202,7 +202,7 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){...on EntityCollectionSection{title artwork(params:$movieResultParam)}...on GallerySection{artwork(params:$movieResultParam)}}}", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, @@ -264,7 +264,7 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations:[_Any!]!$articleResultParam:String){_entities(representations:$representations){...on GallerySection{artwork(params:$articleResultParam)}...on EntityCollectionSection{artwork(params:$articleResultParam)title}}}", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap new file mode 100644 index 0000000000..41a6433f9f --- /dev/null +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap @@ -0,0 +1,193 @@ +--- +source: apollo-router/tests/type_conditions.rs +expression: response +--- +{ + "data": { + "search": [ + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "movieResultEnabled artwork" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "movieResultEnabled artwork" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + }, + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "movieResultEnabled artwork" + }, + { + "artwork": "movieResultEnabled artwork", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ] + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "searchSubgraph", + "variableUsages": [], + "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operationName": "Search__searchSubgraph__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "587c887350ef75eaf4b647be94fd682616bcd33909e15fb797cee226e95fa36a", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "path": [ + "search", + "@|[ArticleResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "articleResultParam" + ], + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", + "operationName": "Search__artworkSubgraph__1", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "a0bf36d3a611df53c3a60b9b124a2887f2d266858221c606ace0985d101d64bd", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + }, + { + "kind": "Flatten", + "path": [ + "search", + "@|[MovieResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "movieResultParam" + ], + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operationName": "Search__artworkSubgraph__2", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "3e84a53f967bf40d4c08254a94f3fa32a828ab3ad8184a22bb3439c596ecaaf4", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \"search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n Flatten(path: \"search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_shouldnt_make_article_fetch.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch.snap similarity index 90% rename from apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_shouldnt_make_article_fetch.snap rename to apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch.snap index acd8fb6676..c8fe1fb487 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_shouldnt_make_article_fetch.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch.snap @@ -47,7 +47,7 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0{search{__typename ...on MovieResult{sections{__typename ...on EntityCollectionSection{__typename id}...on GallerySection{__typename id}}id}...on ArticleResult{id sections{__typename ...on GallerySection{__typename id}...on EntityCollectionSection{__typename id}}}}}", + "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, @@ -109,7 +109,7 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){...on EntityCollectionSection{title artwork(params:$movieResultParam)}...on GallerySection{artwork(params:$movieResultParam)}}}", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, @@ -169,7 +169,7 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations:[_Any!]!$articleResultParam:String){_entities(representations:$representations){...on GallerySection{artwork(params:$articleResultParam)}...on EntityCollectionSection{artwork(params:$articleResultParam)title}}}", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, diff --git a/apollo-router/tests/type_conditions.rs b/apollo-router/tests/type_conditions.rs index aa109807d2..88757228b8 100644 --- a/apollo-router/tests/type_conditions.rs +++ b/apollo-router/tests/type_conditions.rs @@ -2,6 +2,7 @@ //! Please ensure that any tests added to this file use the tokio multi-threaded test executor. //! +use apollo_compiler::ast::Document; use apollo_router::graphql::Request; use apollo_router::graphql::Response; use apollo_router::plugin::test::MockSubgraph; @@ -29,9 +30,45 @@ struct RequestAndResponse { #[tokio::test(flavor = "multi_thread")] async fn test_type_conditions_enabled() { + _test_type_conditions_enabled("legacy").await; + _test_type_conditions_enabled("new").await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_type_conditions_enabled_generate_query_fragments() { + _test_type_conditions_enabled_generate_query_fragments("legacy").await; + _test_type_conditions_enabled_generate_query_fragments("new").await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_type_conditions_enabled_list_of_list() { + _test_type_conditions_enabled_list_of_list("legacy").await; + _test_type_conditions_enabled_list_of_list("new").await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_type_conditions_enabled_list_of_list_of_list() { + _test_type_conditions_enabled_list_of_list_of_list("legacy").await; + _test_type_conditions_enabled_list_of_list_of_list("new").await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_type_conditions_disabled() { + _test_type_conditions_disabled("legacy").await; + _test_type_conditions_disabled("new").await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_type_conditions_enabled_shouldnt_make_article_fetch() { + _test_type_conditions_enabled_shouldnt_make_article_fetch("legacy").await; + _test_type_conditions_enabled_shouldnt_make_article_fetch("new").await; +} + +async fn _test_type_conditions_enabled(planner_mode: &str) -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": true, + "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -70,14 +107,17 @@ async fn test_type_conditions_enabled() { .await .unwrap(); + let response = normalize_response_extensions(response); + insta::assert_json_snapshot!(response); + response } -#[tokio::test(flavor = "multi_thread")] -async fn test_type_conditions_enabled_generate_query_fragments() { +async fn _test_type_conditions_enabled_generate_query_fragments(planner_mode: &str) -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": true, + "experimental_query_planner_mode": planner_mode, "supergraph": { "generate_query_fragments": true }, @@ -119,14 +159,17 @@ async fn test_type_conditions_enabled_generate_query_fragments() { .await .unwrap(); + let response = normalize_response_extensions(response); + insta::assert_json_snapshot!(response); + response } -#[tokio::test(flavor = "multi_thread")] -async fn test_type_conditions_enabled_list_of_list() { +async fn _test_type_conditions_enabled_list_of_list(planner_mode: &str) -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": true, + "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -165,15 +208,18 @@ async fn test_type_conditions_enabled_list_of_list() { .await .unwrap(); + let response = normalize_response_extensions(response); + insta::assert_json_snapshot!(response); + response } // one last to make sure unnesting is correct -#[tokio::test(flavor = "multi_thread")] -async fn test_type_conditions_enabled_list_of_list_of_list() { +async fn _test_type_conditions_enabled_list_of_list_of_list(planner_mode: &str) -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": true, + "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -212,14 +258,17 @@ async fn test_type_conditions_enabled_list_of_list_of_list() { .await .unwrap(); + let response = normalize_response_extensions(response); + insta::assert_json_snapshot!(response); + response } -#[tokio::test(flavor = "multi_thread")] -async fn test_type_conditions_disabled() { +async fn _test_type_conditions_disabled(planner_mode: &str) -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": false, + "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -257,14 +306,17 @@ async fn test_type_conditions_disabled() { .await .unwrap(); + let response = normalize_response_extensions(response); + insta::assert_json_snapshot!(response); + response } -#[tokio::test(flavor = "multi_thread")] -async fn test_type_conditions_enabled_shouldnt_make_article_fetch() { +async fn _test_type_conditions_enabled_shouldnt_make_article_fetch(planner_mode: &str) -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": true, + "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -303,7 +355,10 @@ async fn test_type_conditions_enabled_shouldnt_make_article_fetch() { .await .unwrap(); + let response = normalize_response_extensions(response); + insta::assert_json_snapshot!(response); + response } fn setup_from_mocks( @@ -431,3 +486,45 @@ query Search($movieResultParam: String, $articleResultParam: String) { } } }"#; + +fn normalize_response_extensions(mut response: Response) -> Response { + let extensions = &mut response.extensions; + + for (key, value) in extensions.iter_mut() { + visit_object(key, value, &mut |key, value| { + if key.as_str() == "operation" { + if let Value::String(s) = value { + let new_value = Document::parse(s.as_str(), key.as_str()) + .unwrap() + .serialize() + .no_indent() + .to_string(); + *value = Value::String(new_value.into()); + } + } + }); + } + response +} + +fn visit_object(key: &ByteString, value: &mut Value, cb: &mut impl FnMut(&ByteString, &mut Value)) { + cb(key, value); + + match value { + Value::Object(o) => { + for (key, value) in o.iter_mut() { + visit_object(key, value, cb); + } + } + Value::Array(a) => { + for v in a.iter_mut() { + if let Some(m) = v.as_object_mut() { + for (k, v) in m.iter_mut() { + visit_object(k, v, cb); + } + } + } + } + _ => {} + } +} From 91375ed30cab587227e49378290353675bbe6e44 Mon Sep 17 00:00:00 2001 From: Taylor Ninesling Date: Fri, 13 Sep 2024 11:46:16 -0500 Subject: [PATCH 22/56] Include request variables in demand control scoring (#5995) --- ...ninesling_demand_control_variable_check.md | 5 + apollo-router/src/graphql/visitor.rs | 69 ++- apollo-router/src/json_ext.rs | 41 ++ .../cost_calculator/directives.rs | 14 +- ...ery_with_variable_slicing_argument.graphql | 20 + .../cost_calculator/static_cost.rs | 433 +++++++++++------- .../src/plugins/demand_control/mod.rs | 43 +- .../strategy/static_estimated.rs | 14 +- .../telemetry/config_new/graphql/mod.rs | 20 +- .../telemetry/metrics/local_type_stats.rs | 12 +- apollo-router/src/plugins/telemetry/mod.rs | 11 +- 11 files changed, 475 insertions(+), 207 deletions(-) create mode 100644 .changesets/fix_tninesling_demand_control_variable_check.md create mode 100644 apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_query_with_variable_slicing_argument.graphql diff --git a/.changesets/fix_tninesling_demand_control_variable_check.md b/.changesets/fix_tninesling_demand_control_variable_check.md new file mode 100644 index 0000000000..96d7f81270 --- /dev/null +++ b/.changesets/fix_tninesling_demand_control_variable_check.md @@ -0,0 +1,5 @@ +### Include request variables in demand control scoring ([PR #5995](https://github.com/apollographql/router/pull/5995)) + +Fix demand control scoring for queries which use variables. + +By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/5995 diff --git a/apollo-router/src/graphql/visitor.rs b/apollo-router/src/graphql/visitor.rs index ee92b8741f..b344393821 100644 --- a/apollo-router/src/graphql/visitor.rs +++ b/apollo-router/src/graphql/visitor.rs @@ -1,13 +1,13 @@ -use serde_json_bytes::ByteString; -use serde_json_bytes::Map; use serde_json_bytes::Value; use crate::graphql::Response; +use crate::json_ext::Object; pub(crate) trait ResponseVisitor { fn visit_field( &mut self, request: &apollo_compiler::ExecutableDocument, + variables: &Object, _ty: &apollo_compiler::executable::NamedType, field: &apollo_compiler::executable::Field, value: &Value, @@ -15,11 +15,17 @@ pub(crate) trait ResponseVisitor { match value { Value::Array(items) => { for item in items { - self.visit_list_item(request, field.ty().inner_named_type(), field, item); + self.visit_list_item( + request, + variables, + field.ty().inner_named_type(), + field, + item, + ); } } Value::Object(children) => { - self.visit_selections(request, &field.selection_set, children); + self.visit_selections(request, variables, &field.selection_set, children); } _ => {} } @@ -28,6 +34,7 @@ pub(crate) trait ResponseVisitor { fn visit_list_item( &mut self, request: &apollo_compiler::ExecutableDocument, + variables: &Object, _ty: &apollo_compiler::executable::NamedType, field: &apollo_compiler::executable::Field, value: &Value, @@ -35,17 +42,22 @@ pub(crate) trait ResponseVisitor { match value { Value::Array(items) => { for item in items { - self.visit_list_item(request, _ty, field, item); + self.visit_list_item(request, variables, _ty, field, item); } } Value::Object(children) => { - self.visit_selections(request, &field.selection_set, children); + self.visit_selections(request, variables, &field.selection_set, children); } _ => {} } } - fn visit(&mut self, request: &apollo_compiler::ExecutableDocument, response: &Response) { + fn visit( + &mut self, + request: &apollo_compiler::ExecutableDocument, + response: &Response, + variables: &Object, + ) { if response.path.is_some() { // TODO: In this case, we need to find the selection inside `request` corresponding to the path so we can start zipping. // Exiting here means any implementing visitor will not operate on deffered responses. @@ -54,10 +66,10 @@ pub(crate) trait ResponseVisitor { if let Some(Value::Object(children)) = &response.data { if let Some(operation) = &request.operations.anonymous { - self.visit_selections(request, &operation.selection_set, children); + self.visit_selections(request, variables, &operation.selection_set, children); } for operation in request.operations.named.values() { - self.visit_selections(request, &operation.selection_set, children); + self.visit_selections(request, variables, &operation.selection_set, children); } } } @@ -65,21 +77,28 @@ pub(crate) trait ResponseVisitor { fn visit_selections( &mut self, request: &apollo_compiler::ExecutableDocument, + variables: &Object, selection_set: &apollo_compiler::executable::SelectionSet, - fields: &Map, + fields: &Object, ) { for selection in &selection_set.selections { match selection { apollo_compiler::executable::Selection::Field(inner_field) => { if let Some(value) = fields.get(inner_field.name.as_str()) { - self.visit_field(request, &selection_set.ty, inner_field.as_ref(), value); + self.visit_field( + request, + variables, + &selection_set.ty, + inner_field.as_ref(), + value, + ); } else { tracing::warn!("The response did not include a field corresponding to query field {:?}", inner_field); } } apollo_compiler::executable::Selection::FragmentSpread(fragment_spread) => { if let Some(fragment) = fragment_spread.fragment_def(request) { - self.visit_selections(request, &fragment.selection_set, fields); + self.visit_selections(request, variables, &fragment.selection_set, fields); } else { tracing::warn!( "The fragment {} was not found in the query document.", @@ -88,7 +107,12 @@ pub(crate) trait ResponseVisitor { } } apollo_compiler::executable::Selection::InlineFragment(inline_fragment) => { - self.visit_selections(request, &inline_fragment.selection_set, fields); + self.visit_selections( + request, + variables, + &inline_fragment.selection_set, + fields, + ); } } } @@ -121,7 +145,7 @@ mod tests { let response = Response::from_bytes("test", Bytes::from_static(response_bytes)).unwrap(); let mut visitor = FieldCounter::new(); - visitor.visit(&request, &response); + visitor.visit(&request, &response, &Default::default()); insta::with_settings!({sort_maps=>true}, { assert_yaml_snapshot!(visitor) }) } @@ -136,7 +160,7 @@ mod tests { let response = Response::from_bytes("test", Bytes::from_static(response_bytes)).unwrap(); let mut visitor = FieldCounter::new(); - visitor.visit(&request, &response); + visitor.visit(&request, &response, &Default::default()); insta::with_settings!({sort_maps=>true}, { assert_yaml_snapshot!(visitor) }) } @@ -151,7 +175,7 @@ mod tests { let response = Response::from_bytes("test", Bytes::from_static(response_bytes)).unwrap(); let mut visitor = FieldCounter::new(); - visitor.visit(&request, &response); + visitor.visit(&request, &response, &Default::default()); insta::with_settings!({sort_maps=>true}, { assert_yaml_snapshot!(visitor) }) } @@ -166,7 +190,7 @@ mod tests { let response = Response::from_bytes("test", Bytes::from_static(response_bytes)).unwrap(); let mut visitor = FieldCounter::new(); - visitor.visit(&request, &response); + visitor.visit(&request, &response, &Default::default()); insta::with_settings!({sort_maps=>true}, { assert_yaml_snapshot!(visitor) }) } @@ -186,6 +210,7 @@ mod tests { fn visit_field( &mut self, request: &ExecutableDocument, + variables: &Object, _ty: &apollo_compiler::executable::NamedType, field: &apollo_compiler::executable::Field, value: &Value, @@ -195,11 +220,17 @@ mod tests { match value { Value::Array(items) => { for item in items { - self.visit_list_item(request, field.ty().inner_named_type(), field, item); + self.visit_list_item( + request, + variables, + field.ty().inner_named_type(), + field, + item, + ); } } Value::Object(children) => { - self.visit_selections(request, &field.selection_set, children); + self.visit_selections(request, variables, &field.selection_set, children); } _ => {} } diff --git a/apollo-router/src/json_ext.rs b/apollo-router/src/json_ext.rs index eead336217..c9e617635f 100644 --- a/apollo-router/src/json_ext.rs +++ b/apollo-router/src/json_ext.rs @@ -5,6 +5,7 @@ use std::cmp::min; use std::fmt; +use num_traits::ToPrimitive; use once_cell::sync::Lazy; use regex::Captures; use regex::Regex; @@ -144,6 +145,11 @@ pub(crate) trait ValueExt { /// function to handle `PathElement::Fragment`). #[track_caller] fn is_object_of_type(&self, schema: &Schema, maybe_type: &str) -> bool; + + /// Convert this value to an instance of `apollo_compiler::ast::Value` + fn to_ast(&self) -> apollo_compiler::ast::Value; + + fn as_i32(&self) -> Option; } impl ValueExt for Value { @@ -468,6 +474,41 @@ impl ValueExt for Value { typename == maybe_type || schema.is_subtype(maybe_type, typename) }) } + + fn to_ast(&self) -> apollo_compiler::ast::Value { + match self { + Value::Null => apollo_compiler::ast::Value::Null, + Value::Bool(b) => apollo_compiler::ast::Value::Boolean(*b), + Value::Number(n) if n.is_f64() => { + apollo_compiler::ast::Value::Float(n.as_f64().expect("is float").into()) + } + Value::Number(n) => { + apollo_compiler::ast::Value::Int((n.as_i64().expect("is int") as i32).into()) + } + Value::String(s) => apollo_compiler::ast::Value::String(s.as_str().to_string()), + Value::Array(inner_vars) => apollo_compiler::ast::Value::List( + inner_vars + .iter() + .map(|v| apollo_compiler::Node::new(v.to_ast())) + .collect(), + ), + Value::Object(inner_vars) => apollo_compiler::ast::Value::Object( + inner_vars + .iter() + .map(|(k, v)| { + ( + apollo_compiler::Name::new(k.as_str()).expect("is valid name"), + apollo_compiler::Node::new(v.to_ast()), + ) + }) + .collect(), + ), + } + } + + fn as_i32(&self) -> Option { + self.as_i64()?.to_i32() + } } fn filter_type_conditions(value: Value, type_conditions: &Option) -> Value { diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs b/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs index c4dcc36b00..6f23fcdc00 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs @@ -17,7 +17,9 @@ use apollo_federation::link::spec::APOLLO_SPEC_DOMAIN; use apollo_federation::link::Link; use tower::BoxError; -use super::DemandControlError; +use crate::json_ext::Object; +use crate::json_ext::ValueExt; +use crate::plugins::demand_control::DemandControlError; const COST_DIRECTIVE_NAME: Name = name!("cost"); const COST_DIRECTIVE_DEFAULT_NAME: Name = name!("federation__cost"); @@ -203,9 +205,10 @@ impl DefinitionListSizeDirective { } } - pub(in crate::plugins::demand_control) fn with_field( + pub(in crate::plugins::demand_control) fn with_field_and_variables( &self, field: &Field, + variables: &Object, ) -> Result { let mut slicing_arguments: HashMap<&str, i32> = HashMap::new(); if let Some(slicing_argument_names) = self.slicing_argument_names.as_ref() { @@ -224,6 +227,13 @@ impl DefinitionListSizeDirective { if slicing_argument_names.contains(argument.name.as_str()) { if let Some(numeric_value) = argument.value.to_i32() { slicing_arguments.insert(&argument.name, numeric_value); + } else if let Some(numeric_value) = argument + .value + .as_variable() + .and_then(|variable_name| variables.get(variable_name.as_str())) + .and_then(|variable| variable.as_i32()) + { + slicing_arguments.insert(&argument.name, numeric_value); } } } diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_query_with_variable_slicing_argument.graphql b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_query_with_variable_slicing_argument.graphql new file mode 100644 index 0000000000..c3629cb0e0 --- /dev/null +++ b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_query_with_variable_slicing_argument.graphql @@ -0,0 +1,20 @@ +fragment Items on SizedField { + items { + id + } +} + +query VariableTestQuery($costlyInput: InputTypeWithCost, $fieldCountVar: Int) { + fieldWithCost + argWithCost(arg: 3) + enumWithCost + inputWithCost(someInput: $costlyInput) + scalarWithCost + objectWithCost { + id + } + fieldWithListSize + fieldWithDynamicListSize(first: $fieldCountVar) { + ...Items + } +} diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs index 439d09558f..9da40bb675 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs @@ -21,6 +21,8 @@ use super::schema::DemandControlledSchema; use super::DemandControlError; use crate::graphql::Response; use crate::graphql::ResponseVisitor; +use crate::json_ext::Object; +use crate::json_ext::ValueExt; use crate::plugins::demand_control::cost_calculator::directives::CostDirective; use crate::plugins::demand_control::cost_calculator::directives::ListSizeDirective; use crate::query_planner::fetch::SubgraphOperation; @@ -35,10 +37,18 @@ pub(crate) struct StaticCostCalculator { subgraph_schemas: Arc>, } +struct ScoringContext<'a> { + schema: &'a DemandControlledSchema, + query: &'a ExecutableDocument, + variables: &'a Object, + should_estimate_requires: bool, +} + fn score_argument( argument: &apollo_compiler::ast::Value, argument_definition: &Node, schema: &DemandControlledSchema, + variables: &Object, ) -> Result { let cost_directive = CostDirective::from_argument(schema.directive_name_map(), argument_definition); @@ -74,17 +84,26 @@ fn score_argument( argument_definition.ty.inner_named_type() )) })?; - cost += score_argument(arg_val, arg_def, schema)?; + cost += score_argument(arg_val, arg_def, schema, variables,)?; } Ok(cost) } (ast::Value::List(inner_args), _) => { let mut cost = cost_directive.map_or(0.0, |cost| cost.weight()); for arg_val in inner_args { - cost += score_argument(arg_val, argument_definition, schema)?; + cost += score_argument(arg_val, argument_definition, schema, variables)?; } Ok(cost) } + (ast::Value::Variable(name), _) => { + // We make a best effort attempt to score the variable, but some of these may not exist in the variables + // sent on the supergraph request, such as `$representations`. + if let Some(variable) = variables.get(name.as_str()) { + score_argument(&variable.to_ast(), argument_definition, schema, variables) + } else { + Ok(0.0) + } + } (ast::Value::Null, _) => Ok(0.0), _ => Ok(cost_directive.map_or(0.0, |cost| cost.weight())) } @@ -123,11 +142,9 @@ impl StaticCostCalculator { /// bound for cost anyway. fn score_field( &self, + ctx: &ScoringContext, field: &Field, parent_type: &NamedType, - schema: &DemandControlledSchema, - executable: &ExecutableDocument, - should_estimate_requires: bool, list_size_from_upstream: Option, ) -> Result { if StaticCostCalculator::skipped_by_directives(field) { @@ -136,19 +153,21 @@ impl StaticCostCalculator { // We need to look up the `FieldDefinition` from the supergraph schema instead of using `field.definition` // because `field.definition` was generated from the API schema, which strips off the directives we need. - let definition = schema.type_field(parent_type, &field.name)?; - let ty = field.inner_type_def(schema).ok_or_else(|| { + let definition = ctx.schema.type_field(parent_type, &field.name)?; + let ty = field.inner_type_def(ctx.schema).ok_or_else(|| { DemandControlError::QueryParseFailure(format!( "Field {} was found in query, but its type is missing from the schema.", field.name )) })?; - let list_size_directive = - match schema.type_field_list_size_directive(parent_type, &field.name) { - Some(dir) => dir.with_field(field).map(Some), - None => Ok(None), - }?; + let list_size_directive = match ctx + .schema + .type_field_list_size_directive(parent_type, &field.name) + { + Some(dir) => dir.with_field_and_variables(field, ctx.variables).map(Some), + None => Ok(None), + }?; let instance_count = if !field.ty().is_list() { 1 } else if let Some(value) = list_size_from_upstream { @@ -165,8 +184,9 @@ impl StaticCostCalculator { // Determine the cost for this particular field. Scalars are free, non-scalars are not. // For fields with selections, add in the cost of the selections as well. - let mut type_cost = if let Some(cost_directive) = - schema.type_field_cost_directive(parent_type, &field.name) + let mut type_cost = if let Some(cost_directive) = ctx + .schema + .type_field_cost_directive(parent_type, &field.name) { cost_directive.weight() } else if ty.is_interface() || ty.is_object() || ty.is_union() { @@ -175,11 +195,9 @@ impl StaticCostCalculator { 0.0 }; type_cost += self.score_selection_set( + ctx, &field.selection_set, field.ty().inner_named_type(), - schema, - executable, - should_estimate_requires, list_size_directive.as_ref(), )?; @@ -192,24 +210,28 @@ impl StaticCostCalculator { argument.name, field.name )) })?; - arguments_cost += score_argument(&argument.value, argument_definition, schema)?; + arguments_cost += score_argument( + &argument.value, + argument_definition, + ctx.schema, + ctx.variables, + )?; } let mut requirements_cost = 0.0; - if should_estimate_requires { + if ctx.should_estimate_requires { // If the field is marked with `@requires`, the required selection may not be included // in the query's selection. Adding that requirement's cost to the field ensures it's // accounted for. - let requirements = schema + let requirements = ctx + .schema .type_field_requires_directive(parent_type, &field.name) .map(|d| &d.fields); if let Some(selection_set) = requirements { requirements_cost = self.score_selection_set( + ctx, selection_set, parent_type, - schema, - executable, - should_estimate_requires, list_size_directive.as_ref(), )?; } @@ -231,44 +253,36 @@ impl StaticCostCalculator { fn score_fragment_spread( &self, + ctx: &ScoringContext, fragment_spread: &FragmentSpread, parent_type: &NamedType, - schema: &DemandControlledSchema, - executable: &ExecutableDocument, - should_estimate_requires: bool, list_size_directive: Option<&ListSizeDirective>, ) -> Result { - let fragment = fragment_spread.fragment_def(executable).ok_or_else(|| { + let fragment = fragment_spread.fragment_def(ctx.query).ok_or_else(|| { DemandControlError::QueryParseFailure(format!( "Parsed operation did not have a definition for fragment {}", fragment_spread.fragment_name )) })?; self.score_selection_set( + ctx, &fragment.selection_set, parent_type, - schema, - executable, - should_estimate_requires, list_size_directive, ) } fn score_inline_fragment( &self, + ctx: &ScoringContext, inline_fragment: &InlineFragment, parent_type: &NamedType, - schema: &DemandControlledSchema, - executable: &ExecutableDocument, - should_estimate_requires: bool, list_size_directive: Option<&ListSizeDirective>, ) -> Result { self.score_selection_set( + ctx, &inline_fragment.selection_set, parent_type, - schema, - executable, - should_estimate_requires, list_size_directive, ) } @@ -276,63 +290,43 @@ impl StaticCostCalculator { fn score_operation( &self, operation: &Operation, - schema: &DemandControlledSchema, - executable: &ExecutableDocument, - should_estimate_requires: bool, + ctx: &ScoringContext, ) -> Result { let mut cost = if operation.is_mutation() { 10.0 } else { 0.0 }; - let Some(root_type_name) = schema.root_operation(operation.operation_type) else { + let Some(root_type_name) = ctx.schema.root_operation(operation.operation_type) else { return Err(DemandControlError::QueryParseFailure(format!( "Cannot cost {} operation because the schema does not support this root type", operation.operation_type ))); }; - cost += self.score_selection_set( - &operation.selection_set, - root_type_name, - schema, - executable, - should_estimate_requires, - None, - )?; + cost += self.score_selection_set(ctx, &operation.selection_set, root_type_name, None)?; Ok(cost) } fn score_selection( &self, + ctx: &ScoringContext, selection: &Selection, parent_type: &NamedType, - schema: &DemandControlledSchema, - executable: &ExecutableDocument, - should_estimate_requires: bool, list_size_directive: Option<&ListSizeDirective>, ) -> Result { match selection { Selection::Field(f) => self.score_field( + ctx, f, parent_type, - schema, - executable, - should_estimate_requires, list_size_directive.and_then(|dir| dir.size_of(f)), ), - Selection::FragmentSpread(s) => self.score_fragment_spread( - s, - parent_type, - schema, - executable, - should_estimate_requires, - list_size_directive, - ), + Selection::FragmentSpread(s) => { + self.score_fragment_spread(ctx, s, parent_type, list_size_directive) + } Selection::InlineFragment(i) => self.score_inline_fragment( + ctx, i, i.type_condition.as_ref().unwrap_or(parent_type), - schema, - executable, - should_estimate_requires, list_size_directive, ), } @@ -340,23 +334,14 @@ impl StaticCostCalculator { fn score_selection_set( &self, + ctx: &ScoringContext, selection_set: &SelectionSet, parent_type_name: &NamedType, - schema: &DemandControlledSchema, - executable: &ExecutableDocument, - should_estimate_requires: bool, list_size_directive: Option<&ListSizeDirective>, ) -> Result { let mut cost = 0.0; for selection in selection_set.selections.iter() { - cost += self.score_selection( - selection, - parent_type_name, - schema, - executable, - should_estimate_requires, - list_size_directive, - )?; + cost += self.score_selection(ctx, selection, parent_type_name, list_size_directive)?; } Ok(cost) } @@ -375,25 +360,33 @@ impl StaticCostCalculator { false } - fn score_plan_node(&self, plan_node: &PlanNode) -> Result { + fn score_plan_node( + &self, + plan_node: &PlanNode, + variables: &Object, + ) -> Result { match plan_node { - PlanNode::Sequence { nodes } => self.summed_score_of_nodes(nodes), - PlanNode::Parallel { nodes } => self.summed_score_of_nodes(nodes), - PlanNode::Flatten(flatten_node) => self.score_plan_node(&flatten_node.node), + PlanNode::Sequence { nodes } => self.summed_score_of_nodes(nodes, variables), + PlanNode::Parallel { nodes } => self.summed_score_of_nodes(nodes, variables), + PlanNode::Flatten(flatten_node) => self.score_plan_node(&flatten_node.node, variables), PlanNode::Condition { condition: _, if_clause, else_clause, - } => self.max_score_of_nodes(if_clause, else_clause), + } => self.max_score_of_nodes(if_clause, else_clause, variables), PlanNode::Defer { primary, deferred } => { - self.summed_score_of_deferred_nodes(primary, deferred) - } - PlanNode::Fetch(fetch_node) => { - self.estimated_cost_of_operation(&fetch_node.service_name, &fetch_node.operation) - } - PlanNode::Subscription { primary, rest: _ } => { - self.estimated_cost_of_operation(&primary.service_name, &primary.operation) + self.summed_score_of_deferred_nodes(primary, deferred, variables) } + PlanNode::Fetch(fetch_node) => self.estimated_cost_of_operation( + &fetch_node.service_name, + &fetch_node.operation, + variables, + ), + PlanNode::Subscription { primary, rest: _ } => self.estimated_cost_of_operation( + &primary.service_name, + &primary.operation, + variables, + ), } } @@ -401,6 +394,7 @@ impl StaticCostCalculator { &self, subgraph: &str, operation: &SubgraphOperation, + variables: &Object, ) -> Result { tracing::debug!("On subgraph {}, scoring operation: {}", subgraph, operation); @@ -414,21 +408,22 @@ impl StaticCostCalculator { let operation = operation .as_parsed() .map_err(DemandControlError::SubgraphOperationNotInitialized)?; - self.estimated(operation, schema, false) + self.estimated(operation, schema, variables, false) } fn max_score_of_nodes( &self, left: &Option>, right: &Option>, + variables: &Object, ) -> Result { match (left, right) { (None, None) => Ok(0.0), - (None, Some(right)) => self.score_plan_node(right), - (Some(left), None) => self.score_plan_node(left), + (None, Some(right)) => self.score_plan_node(right, variables), + (Some(left), None) => self.score_plan_node(left, variables), (Some(left), Some(right)) => { - let left_score = self.score_plan_node(left)?; - let right_score = self.score_plan_node(right)?; + let left_score = self.score_plan_node(left, variables)?; + let right_score = self.score_plan_node(right, variables)?; Ok(left_score.max(right_score)) } } @@ -438,23 +433,28 @@ impl StaticCostCalculator { &self, primary: &Primary, deferred: &Vec, + variables: &Object, ) -> Result { let mut score = 0.0; if let Some(node) = &primary.node { - score += self.score_plan_node(node)?; + score += self.score_plan_node(node, variables)?; } for d in deferred { if let Some(node) = &d.node { - score += self.score_plan_node(node)?; + score += self.score_plan_node(node, variables)?; } } Ok(score) } - fn summed_score_of_nodes(&self, nodes: &Vec) -> Result { + fn summed_score_of_nodes( + &self, + nodes: &Vec, + variables: &Object, + ) -> Result { let mut sum = 0.0; for node in nodes { - sum += self.score_plan_node(node)?; + sum += self.score_plan_node(node, variables)?; } Ok(sum) } @@ -463,29 +463,41 @@ impl StaticCostCalculator { &self, query: &ExecutableDocument, schema: &DemandControlledSchema, + variables: &Object, should_estimate_requires: bool, ) -> Result { let mut cost = 0.0; + let ctx = ScoringContext { + schema, + query, + variables, + should_estimate_requires, + }; if let Some(op) = &query.operations.anonymous { - cost += self.score_operation(op, schema, query, should_estimate_requires)?; + cost += self.score_operation(op, &ctx)?; } for (_name, op) in query.operations.named.iter() { - cost += self.score_operation(op, schema, query, should_estimate_requires)?; + cost += self.score_operation(op, &ctx)?; } Ok(cost) } - pub(crate) fn planned(&self, query_plan: &QueryPlan) -> Result { - self.score_plan_node(&query_plan.root) + pub(crate) fn planned( + &self, + query_plan: &QueryPlan, + variables: &Object, + ) -> Result { + self.score_plan_node(&query_plan.root, variables) } pub(crate) fn actual( &self, request: &ExecutableDocument, response: &Response, + variables: &Object, ) -> Result { let mut visitor = ResponseCostCalculator::new(&self.supergraph_schema); - visitor.visit(request, response); + visitor.visit(request, response, variables); Ok(visitor.cost) } } @@ -505,11 +517,12 @@ impl<'schema> ResponseVisitor for ResponseCostCalculator<'schema> { fn visit_field( &mut self, request: &ExecutableDocument, + variables: &Object, parent_ty: &NamedType, field: &Field, value: &Value, ) { - self.visit_list_item(request, parent_ty, field, value); + self.visit_list_item(request, variables, parent_ty, field, value); let definition = self.schema.type_field(parent_ty, &field.name); for argument in &field.arguments { @@ -517,7 +530,8 @@ impl<'schema> ResponseVisitor for ResponseCostCalculator<'schema> { .as_ref() .map(|def| def.argument_by_name(&argument.name)) { - if let Ok(score) = score_argument(&argument.value, argument_definition, self.schema) + if let Ok(score) = + score_argument(&argument.value, argument_definition, self.schema, variables) { self.cost += score; } @@ -534,6 +548,7 @@ impl<'schema> ResponseVisitor for ResponseCostCalculator<'schema> { fn visit_list_item( &mut self, request: &apollo_compiler::ExecutableDocument, + variables: &Object, parent_ty: &apollo_compiler::executable::NamedType, field: &apollo_compiler::executable::Field, value: &Value, @@ -548,12 +563,12 @@ impl<'schema> ResponseVisitor for ResponseCostCalculator<'schema> { } Value::Array(items) => { for item in items { - self.visit_list_item(request, parent_ty, field, item); + self.visit_list_item(request, variables, parent_ty, field, item); } } Value::Object(children) => { self.cost += cost_directive.map_or(1.0, |cost| cost.weight()); - self.visit_selections(request, &field.selection_set, children); + self.visit_selections(request, variables, &field.selection_set, children); } } } @@ -583,9 +598,10 @@ mod tests { fn rust_planned( &self, query_plan: &apollo_federation::query_plan::QueryPlan, + variables: &Object, ) -> Result { let js_planner_node: PlanNode = query_plan.node.as_ref().unwrap().into(); - self.score_plan_node(&js_planner_node) + self.score_plan_node(&js_planner_node, variables) } } @@ -600,20 +616,30 @@ mod tests { } /// Estimate cost of an operation executed on a supergraph. - fn estimated_cost(schema_str: &str, query_str: &str) -> f64 { + fn estimated_cost(schema_str: &str, query_str: &str, variables_str: &str) -> f64 { let (schema, query) = parse_schema_and_operation(schema_str, query_str, &Default::default()); + let variables = serde_json::from_str::(variables_str) + .unwrap() + .as_object() + .cloned() + .unwrap_or_default(); let schema = DemandControlledSchema::new(Arc::new(schema.supergraph_schema().clone())).unwrap(); let calculator = StaticCostCalculator::new(Arc::new(schema), Default::default(), 100); calculator - .estimated(&query.executable, &calculator.supergraph_schema, true) + .estimated( + &query.executable, + &calculator.supergraph_schema, + &variables, + true, + ) .unwrap() } /// Estimate cost of an operation on a plain, non-federated schema. - fn basic_estimated_cost(schema_str: &str, query_str: &str) -> f64 { + fn basic_estimated_cost(schema_str: &str, query_str: &str, variables_str: &str) -> f64 { let schema = apollo_compiler::Schema::parse_and_validate(schema_str, "schema.graphqls").unwrap(); let query = apollo_compiler::ExecutableDocument::parse_and_validate( @@ -622,17 +648,27 @@ mod tests { "query.graphql", ) .unwrap(); + let variables = serde_json::from_str::(variables_str) + .unwrap() + .as_object() + .cloned() + .unwrap_or_default(); let schema = DemandControlledSchema::new(Arc::new(schema)).unwrap(); let calculator = StaticCostCalculator::new(Arc::new(schema), Default::default(), 100); calculator - .estimated(&query, &calculator.supergraph_schema, true) + .estimated(&query, &calculator.supergraph_schema, &variables, true) .unwrap() } - async fn planned_cost_js(schema_str: &str, query_str: &str) -> f64 { + async fn planned_cost_js(schema_str: &str, query_str: &str, variables_str: &str) -> f64 { let config: Arc = Arc::new(Default::default()); let (schema, query) = parse_schema_and_operation(schema_str, query_str, &config); + let variables = serde_json::from_str::(variables_str) + .unwrap() + .as_object() + .cloned() + .unwrap_or_default(); let supergraph_schema = schema.supergraph_schema().clone(); let mut planner = BridgeQueryPlanner::new(schema.into(), config.clone(), None, None) @@ -667,12 +703,17 @@ mod tests { 100, ); - calculator.planned(&query_plan).unwrap() + calculator.planned(&query_plan, &variables).unwrap() } - fn planned_cost_rust(schema_str: &str, query_str: &str) -> f64 { + fn planned_cost_rust(schema_str: &str, query_str: &str, variables_str: &str) -> f64 { let config: Arc = Arc::new(Default::default()); let (schema, query) = parse_schema_and_operation(schema_str, query_str, &config); + let variables = serde_json::from_str::(variables_str) + .unwrap() + .as_object() + .cloned() + .unwrap_or_default(); let planner = QueryPlanner::new(schema.federation_supergraph(), Default::default()).unwrap(); @@ -695,22 +736,37 @@ mod tests { 100, ); - calculator.rust_planned(&query_plan).unwrap() + calculator.rust_planned(&query_plan, &variables).unwrap() } - fn actual_cost(schema_str: &str, query_str: &str, response_bytes: &'static [u8]) -> f64 { + fn actual_cost( + schema_str: &str, + query_str: &str, + variables_str: &str, + response_bytes: &'static [u8], + ) -> f64 { let (schema, query) = parse_schema_and_operation(schema_str, query_str, &Default::default()); + let variables = serde_json::from_str::(variables_str) + .unwrap() + .as_object() + .cloned() + .unwrap_or_default(); let response = Response::from_bytes("test", Bytes::from(response_bytes)).unwrap(); let schema = DemandControlledSchema::new(Arc::new(schema.supergraph_schema().clone())).unwrap(); StaticCostCalculator::new(Arc::new(schema), Default::default(), 100) - .actual(&query.executable, &response) + .actual(&query.executable, &response, &variables) .unwrap() } /// Actual cost of an operation on a plain, non-federated schema. - fn basic_actual_cost(schema_str: &str, query_str: &str, response_bytes: &'static [u8]) -> f64 { + fn basic_actual_cost( + schema_str: &str, + query_str: &str, + variables_str: &str, + response_bytes: &'static [u8], + ) -> f64 { let schema = apollo_compiler::Schema::parse_and_validate(schema_str, "schema.graphqls").unwrap(); let query = apollo_compiler::ExecutableDocument::parse_and_validate( @@ -719,11 +775,16 @@ mod tests { "query.graphql", ) .unwrap(); + let variables = serde_json::from_str::(variables_str) + .unwrap() + .as_object() + .cloned() + .unwrap_or_default(); let response = Response::from_bytes("test", Bytes::from(response_bytes)).unwrap(); let schema = DemandControlledSchema::new(Arc::new(schema)).unwrap(); StaticCostCalculator::new(Arc::new(schema), Default::default(), 100) - .actual(&query, &response) + .actual(&query, &response, &variables) .unwrap() } @@ -731,157 +792,174 @@ mod tests { fn query_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 0.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 0.0) } #[test] fn mutation_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_mutation.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 10.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 10.0) } #[test] fn object_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_object_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 1.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 1.0) } #[test] fn interface_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_interface_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 1.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 1.0) } #[test] fn union_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_union_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 1.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 1.0) } #[test] fn list_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_object_list_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 100.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 100.0) } #[test] fn scalar_list_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_scalar_list_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 0.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 0.0) } #[test] fn nested_object_lists() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_nested_list_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 10100.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 10100.0) } #[test] fn input_object_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_input_object_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 4.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 4.0) } #[test] fn input_object_cost_with_returned_objects() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_input_object_query_2.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/basic_input_object_response.json"); - assert_eq!(basic_estimated_cost(schema, query), 104.0); + assert_eq!(basic_estimated_cost(schema, query, variables), 104.0); // The cost of the arguments from the query should be included when scoring the response - assert_eq!(basic_actual_cost(schema, query, response), 7.0); + assert_eq!(basic_actual_cost(schema, query, variables, response), 7.0); } #[test] fn skip_directive_excludes_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_skipped_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 0.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 0.0) } #[test] fn include_directive_excludes_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_excluded_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 0.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 0.0) } #[test(tokio::test)] async fn federated_query_with_name() { let schema = include_str!("./fixtures/federated_ships_schema.graphql"); let query = include_str!("./fixtures/federated_ships_named_query.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/federated_ships_named_response.json"); - assert_eq!(estimated_cost(schema, query), 100.0); - assert_eq!(actual_cost(schema, query, response), 2.0); + assert_eq!(estimated_cost(schema, query, variables), 100.0); + assert_eq!(actual_cost(schema, query, variables, response), 2.0); } #[test(tokio::test)] async fn federated_query_with_requires() { let schema = include_str!("./fixtures/federated_ships_schema.graphql"); let query = include_str!("./fixtures/federated_ships_required_query.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/federated_ships_required_response.json"); - assert_eq!(estimated_cost(schema, query), 10200.0); - assert_eq!(planned_cost_js(schema, query).await, 10400.0); - assert_eq!(planned_cost_rust(schema, query), 10400.0); - assert_eq!(actual_cost(schema, query, response), 2.0); + assert_eq!(estimated_cost(schema, query, variables), 10200.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 10400.0); + assert_eq!(planned_cost_rust(schema, query, variables), 10400.0); + assert_eq!(actual_cost(schema, query, variables, response), 2.0); } #[test(tokio::test)] async fn federated_query_with_fragments() { let schema = include_str!("./fixtures/federated_ships_schema.graphql"); let query = include_str!("./fixtures/federated_ships_fragment_query.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/federated_ships_fragment_response.json"); - assert_eq!(estimated_cost(schema, query), 300.0); - assert_eq!(planned_cost_js(schema, query).await, 400.0); - assert_eq!(planned_cost_rust(schema, query), 400.0); - assert_eq!(actual_cost(schema, query, response), 6.0); + assert_eq!(estimated_cost(schema, query, variables), 300.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 400.0); + assert_eq!(planned_cost_rust(schema, query, variables), 400.0); + assert_eq!(actual_cost(schema, query, variables, response), 6.0); } #[test(tokio::test)] async fn federated_query_with_inline_fragments() { let schema = include_str!("./fixtures/federated_ships_schema.graphql"); let query = include_str!("./fixtures/federated_ships_inline_fragment_query.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/federated_ships_fragment_response.json"); - assert_eq!(estimated_cost(schema, query), 300.0); - assert_eq!(planned_cost_js(schema, query).await, 400.0); - assert_eq!(planned_cost_rust(schema, query), 400.0); - assert_eq!(actual_cost(schema, query, response), 6.0); + assert_eq!(estimated_cost(schema, query, variables), 300.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 400.0); + assert_eq!(planned_cost_rust(schema, query, variables), 400.0); + assert_eq!(actual_cost(schema, query, variables, response), 6.0); } #[test(tokio::test)] async fn federated_query_with_defer() { let schema = include_str!("./fixtures/federated_ships_schema.graphql"); let query = include_str!("./fixtures/federated_ships_deferred_query.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/federated_ships_deferred_response.json"); - assert_eq!(estimated_cost(schema, query), 10200.0); - assert_eq!(planned_cost_js(schema, query).await, 10400.0); - assert_eq!(planned_cost_rust(schema, query), 10400.0); - assert_eq!(actual_cost(schema, query, response), 2.0); + assert_eq!(estimated_cost(schema, query, variables), 10200.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 10400.0); + assert_eq!(planned_cost_rust(schema, query, variables), 10400.0); + assert_eq!(actual_cost(schema, query, variables, response), 2.0); } #[test(tokio::test)] @@ -895,12 +973,22 @@ mod tests { let calculator = StaticCostCalculator::new(schema.clone(), Default::default(), 100); let conservative_estimate = calculator - .estimated(&query.executable, &calculator.supergraph_schema, true) + .estimated( + &query.executable, + &calculator.supergraph_schema, + &Default::default(), + true, + ) .unwrap(); let calculator = StaticCostCalculator::new(schema.clone(), Default::default(), 5); let narrow_estimate = calculator - .estimated(&query.executable, &calculator.supergraph_schema, true) + .estimated( + &query.executable, + &calculator.supergraph_schema, + &Default::default(), + true, + ) .unwrap(); assert_eq!(conservative_estimate, 10200.0); @@ -911,24 +999,26 @@ mod tests { async fn custom_cost_query() { let schema = include_str!("./fixtures/custom_cost_schema.graphql"); let query = include_str!("./fixtures/custom_cost_query.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/custom_cost_response.json"); - assert_eq!(estimated_cost(schema, query), 127.0); - assert_eq!(planned_cost_js(schema, query).await, 127.0); - assert_eq!(planned_cost_rust(schema, query), 127.0); - assert_eq!(actual_cost(schema, query, response), 125.0); + assert_eq!(estimated_cost(schema, query, variables), 127.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 127.0); + assert_eq!(planned_cost_rust(schema, query, variables), 127.0); + assert_eq!(actual_cost(schema, query, variables, response), 125.0); } #[test(tokio::test)] async fn custom_cost_query_with_renamed_directives() { let schema = include_str!("./fixtures/custom_cost_schema_with_renamed_directives.graphql"); let query = include_str!("./fixtures/custom_cost_query.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/custom_cost_response.json"); - assert_eq!(estimated_cost(schema, query), 127.0); - assert_eq!(planned_cost_js(schema, query).await, 127.0); - assert_eq!(planned_cost_rust(schema, query), 127.0); - assert_eq!(actual_cost(schema, query, response), 125.0); + assert_eq!(estimated_cost(schema, query, variables), 127.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 127.0); + assert_eq!(planned_cost_rust(schema, query, variables), 127.0); + assert_eq!(actual_cost(schema, query, variables, response), 125.0); } #[test(tokio::test)] @@ -936,11 +1026,26 @@ mod tests { let schema = include_str!("./fixtures/custom_cost_schema.graphql"); let query = include_str!("./fixtures/custom_cost_query_with_default_slicing_argument.graphql"); + let variables = "{}"; + let response = include_bytes!("./fixtures/custom_cost_response.json"); + + assert_eq!(estimated_cost(schema, query, variables), 132.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 132.0); + assert_eq!(planned_cost_rust(schema, query, variables), 132.0); + assert_eq!(actual_cost(schema, query, variables, response), 125.0); + } + + #[test(tokio::test)] + async fn custom_cost_query_with_variable_slicing_argument() { + let schema = include_str!("./fixtures/custom_cost_schema.graphql"); + let query = + include_str!("./fixtures/custom_cost_query_with_variable_slicing_argument.graphql"); + let variables = r#"{"costlyInput": {"somethingWithCost": 10}, "fieldCountVar": 5}"#; let response = include_bytes!("./fixtures/custom_cost_response.json"); - assert_eq!(estimated_cost(schema, query), 132.0); - assert_eq!(planned_cost_js(schema, query).await, 132.0); - assert_eq!(planned_cost_rust(schema, query), 132.0); - assert_eq!(actual_cost(schema, query, response), 125.0); + assert_eq!(estimated_cost(schema, query, variables), 127.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 127.0); + assert_eq!(planned_cost_rust(schema, query, variables), 127.0); + assert_eq!(actual_cost(schema, query, variables, response), 125.0); } } diff --git a/apollo-router/src/plugins/demand_control/mod.rs b/apollo-router/src/plugins/demand_control/mod.rs index 0e779cad01..62255fa832 100644 --- a/apollo-router/src/plugins/demand_control/mod.rs +++ b/apollo-router/src/plugins/demand_control/mod.rs @@ -201,6 +201,12 @@ impl<'a> From> for DemandControlError { } } +#[derive(Clone)] +pub(crate) struct DemandControlContext { + pub(crate) strategy: Strategy, + pub(crate) variables: Object, +} + impl Context { pub(crate) fn insert_estimated_cost(&self, cost: f64) -> Result<(), DemandControlError> { self.insert(COST_ESTIMATED_KEY, cost) @@ -251,6 +257,14 @@ impl Context { self.get::<&str, String>(COST_STRATEGY_KEY) .map_err(|e| DemandControlError::ContextSerializationError(e.to_string())) } + + pub(crate) fn insert_demand_control_context(&self, ctx: DemandControlContext) { + self.extensions().with_lock(|mut lock| lock.insert(ctx)); + } + + pub(crate) fn get_demand_control_context(&self) -> Option { + self.extensions().with_lock(|lock| lock.get().cloned()) + } } pub(crate) struct DemandControl { @@ -307,8 +321,11 @@ impl Plugin for DemandControl { ServiceBuilder::new() .checkpoint(move |req: execution::Request| { req.context - .extensions() - .with_lock(|mut lock| lock.insert(strategy.clone())); + .insert_demand_control_context(DemandControlContext { + strategy: strategy.clone(), + variables: req.supergraph_request.body().variables.clone(), + }); + // On the request path we need to check for estimates, checkpoint is used to do this, short-circuiting the request if it's too expensive. Ok(match strategy.on_execution_request(&req) { Ok(_) => ControlFlow::Continue(req), @@ -329,9 +346,11 @@ impl Plugin for DemandControl { .context .unsupported_executable_document() .expect("must have document"); - let strategy = resp.context.extensions().with_lock(|lock| { - lock.get::().expect("must have strategy").clone() - }); + let strategy = resp + .context + .get_demand_control_context() + .map(|ctx| ctx.strategy) + .expect("must have strategy"); let context = resp.context.clone(); // We want to sequence this code to run after all the subgraph responses have been scored. @@ -392,9 +411,7 @@ impl Plugin for DemandControl { let subgraph_name_map_fut = subgraph_name.to_owned(); ServiceBuilder::new() .checkpoint(move |req: subgraph::Request| { - let strategy = req.context.extensions().with_lock(|lock| { - lock.get::().expect("must have strategy").clone() - }); + let strategy = req.context.get_demand_control_context().map(|c| c.strategy).expect("must have strategy"); // On the request path we need to check for estimates, checkpoint is used to do this, short-circuiting the request if it's too expensive. Ok(match strategy.on_subgraph_request(&req) { @@ -424,9 +441,7 @@ impl Plugin for DemandControl { }, |(subgraph_name, req): (String, Arc>), fut| async move { let resp: subgraph::Response = fut.await?; - let strategy = resp.context.extensions().with_lock(|lock| { - lock.get::().expect("must have strategy").clone() - }); + let strategy = resp.context.get_demand_control_context().map(|c| c.strategy).expect("must have strategy"); Ok(match strategy.on_subgraph_response(req.as_ref(), &resp) { Ok(_) => resp, Err(err) => subgraph::Response::builder() @@ -464,6 +479,7 @@ mod test { use crate::graphql::Response; use crate::metrics::FutureMetricsExt; use crate::plugins::demand_control::DemandControl; + use crate::plugins::demand_control::DemandControlContext; use crate::plugins::demand_control::DemandControlError; use crate::plugins::test::PluginTestHarness; use crate::query_planner::fetch::QueryHash; @@ -622,7 +638,10 @@ mod test { let strategy = plugin.strategy_factory.create(); let ctx = context(); - ctx.extensions().with_lock(|mut lock| lock.insert(strategy)); + ctx.insert_demand_control_context(DemandControlContext { + strategy, + variables: Default::default(), + }); let mut req = subgraph::Request::fake_builder() .subgraph_name("test") .context(ctx) diff --git a/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs b/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs index 3ee1894473..f3a114e8fb 100644 --- a/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs +++ b/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs @@ -17,7 +17,10 @@ pub(crate) struct StaticEstimated { impl StrategyImpl for StaticEstimated { fn on_execution_request(&self, request: &execution::Request) -> Result<(), DemandControlError> { self.cost_calculator - .planned(&request.query_plan) + .planned( + &request.query_plan, + &request.supergraph_request.body().variables, + ) .and_then(|cost| { request .context @@ -59,7 +62,14 @@ impl StrategyImpl for StaticEstimated { response: &graphql::Response, ) -> Result<(), DemandControlError> { if response.data.is_some() { - let cost = self.cost_calculator.actual(request, response)?; + let cost = self.cost_calculator.actual( + request, + response, + &context + .extensions() + .with_lock(|lock| lock.get().cloned()) + .unwrap_or_default(), + )?; context.insert_actual_cost(cost)?; } Ok(()) diff --git a/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs b/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs index 7e68446e3d..3cabed05ff 100644 --- a/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs +++ b/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs @@ -9,6 +9,7 @@ use tower::BoxError; use super::instruments::CustomCounter; use super::instruments::CustomInstruments; use crate::graphql::ResponseVisitor; +use crate::json_ext::Object; use crate::plugins::telemetry::config_new::attributes::DefaultAttributeRequirementLevel; use crate::plugins::telemetry::config_new::extendable::Extendable; use crate::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes; @@ -136,7 +137,13 @@ impl Instrumented for GraphQLInstruments { ctx, instruments: self, } - .visit(&executable_document, response); + .visit( + &executable_document, + response, + &ctx.get_demand_control_context() + .map(|c| c.variables) + .unwrap_or_default(), + ); } } } @@ -161,6 +168,7 @@ impl<'a> ResponseVisitor for GraphQLInstrumentsVisitor<'a> { fn visit_field( &mut self, request: &ExecutableDocument, + variables: &Object, ty: &NamedType, field: &Field, value: &Value, @@ -171,11 +179,17 @@ impl<'a> ResponseVisitor for GraphQLInstrumentsVisitor<'a> { match value { Value::Array(items) => { for item in items { - self.visit_list_item(request, field.ty().inner_named_type(), field, item); + self.visit_list_item( + request, + variables, + field.ty().inner_named_type(), + field, + item, + ); } } Value::Object(children) => { - self.visit_selections(request, &field.selection_set, children); + self.visit_selections(request, variables, &field.selection_set, children); } _ => {} } diff --git a/apollo-router/src/plugins/telemetry/metrics/local_type_stats.rs b/apollo-router/src/plugins/telemetry/metrics/local_type_stats.rs index f53e8667be..b586dacf5c 100644 --- a/apollo-router/src/plugins/telemetry/metrics/local_type_stats.rs +++ b/apollo-router/src/plugins/telemetry/metrics/local_type_stats.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use crate::graphql::ResponseVisitor; +use crate::json_ext::Object; use crate::plugins::telemetry::metrics::apollo::histogram::ListLengthHistogram; use crate::plugins::telemetry::metrics::apollo::studio::LocalFieldStat; use crate::plugins::telemetry::metrics::apollo::studio::LocalTypeStat; @@ -22,6 +23,7 @@ impl ResponseVisitor for LocalTypeStatRecorder { fn visit_field( &mut self, request: &apollo_compiler::ExecutableDocument, + variables: &Object, ty: &apollo_compiler::executable::NamedType, field: &apollo_compiler::executable::Field, value: &serde_json_bytes::Value, @@ -61,11 +63,17 @@ impl ResponseVisitor for LocalTypeStatRecorder { .record(Some(items.len() as u64), 1); for item in items { - self.visit_list_item(request, field.ty().inner_named_type(), field, item); + self.visit_list_item( + request, + variables, + field.ty().inner_named_type(), + field, + item, + ); } } serde_json_bytes::Value::Object(children) => { - self.visit_selections(request, &field.selection_set, children); + self.visit_selections(request, variables, &field.selection_set, children); } _ => {} } diff --git a/apollo-router/src/plugins/telemetry/mod.rs b/apollo-router/src/plugins/telemetry/mod.rs index 886bebea1a..1478261be5 100644 --- a/apollo-router/src/plugins/telemetry/mod.rs +++ b/apollo-router/src/plugins/telemetry/mod.rs @@ -95,7 +95,6 @@ use crate::metrics::filter::FilterMeterProvider; use crate::metrics::meter_provider; use crate::plugin::Plugin; use crate::plugin::PluginInit; -use crate::plugins::demand_control; use crate::plugins::telemetry::apollo::ForwardHeaders; use crate::plugins::telemetry::apollo_exporter::proto::reports::trace::node::Id::ResponseName; use crate::plugins::telemetry::apollo_exporter::proto::reports::StatsContext; @@ -1411,7 +1410,13 @@ impl Telemetry { config.apollo.experimental_local_field_metrics, ctx.unsupported_executable_document(), ) { - local_stat_recorder.visit(&query, &response); + local_stat_recorder.visit( + &query, + &response, + &ctx.get_demand_control_context() + .map(|c| c.variables) + .unwrap_or_default(), + ); } if operation_kind == OperationKind::Subscription { @@ -1514,8 +1519,8 @@ impl Telemetry { let traces = Self::subgraph_ftv1_traces(context); let per_type_stat = Self::per_type_stat(&traces, field_level_instrumentation_ratio); let root_error_stats = Self::per_path_error_stats(&traces); + let strategy = context.get_demand_control_context().map(|c| c.strategy); let limits_stats = context.extensions().with_lock(|guard| { - let strategy = guard.get::(); let query_limits = guard.get::>(); SingleLimitsStats { strategy: strategy.and_then(|s| serde_json::to_string(&s.mode).ok()), From 5c5c707265c72e3d67834f422b60097d706288b8 Mon Sep 17 00:00:00 2001 From: Bryn Cooke Date: Mon, 16 Sep 2024 16:26:01 +0100 Subject: [PATCH 23/56] Fix datadog sample propagation (#6005) Co-authored-by: bryn --- apollo-router/src/plugins/telemetry/reload.rs | 5 +-- apollo-router/tests/common.rs | 6 +++ .../tests/integration/telemetry/datadog.rs | 37 +++++++++++++++++++ .../fixtures/datadog_no_sample.router.yaml | 24 ++++++++++++ 4 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 apollo-router/tests/integration/telemetry/fixtures/datadog_no_sample.router.yaml diff --git a/apollo-router/src/plugins/telemetry/reload.rs b/apollo-router/src/plugins/telemetry/reload.rs index 0f529efd2f..2ca69191c9 100644 --- a/apollo-router/src/plugins/telemetry/reload.rs +++ b/apollo-router/src/plugins/telemetry/reload.rs @@ -35,7 +35,6 @@ use crate::plugins::telemetry::formatters::FilteringFormatter; use crate::plugins::telemetry::otel; use crate::plugins::telemetry::otel::OpenTelemetryLayer; use crate::plugins::telemetry::otel::PreSampledTracer; -use crate::plugins::telemetry::tracing::datadog_exporter::DatadogTraceState; use crate::plugins::telemetry::tracing::reload::ReloadTracer; use crate::tracer::TraceId; @@ -141,9 +140,7 @@ pub(crate) fn prepare_context(context: Context) -> Context { tracer.new_span_id(), TraceFlags::default(), false, - TraceState::default() - .with_measuring(true) - .with_priority_sampling(true), + TraceState::default(), ); return context.with_remote_span_context(span_context); } diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index beccfcdddd..42eb5d3c9a 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -103,6 +103,7 @@ struct TracedResponder { response_template: ResponseTemplate, telemetry: Telemetry, subscriber_subgraph: Dispatch, + subgraph_callback: Option>, } impl Respond for TracedResponder { @@ -112,6 +113,9 @@ impl Respond for TracedResponder { let _context_guard = context.attach(); let span = info_span!("subgraph server"); let _span_guard = span.enter(); + if let Some(callback) = &self.subgraph_callback { + callback(); + } self.response_template.clone() }) } @@ -280,6 +284,7 @@ impl IntegrationTest { supergraph: Option, mut subgraph_overrides: HashMap, log: Option, + subgraph_callback: Option>, ) -> Self { let redis_namespace = Uuid::new_v4().to_string(); let telemetry = telemetry.unwrap_or_default(); @@ -313,6 +318,7 @@ impl IntegrationTest { ResponseTemplate::new(200).set_body_json(json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}))), telemetry: telemetry.clone(), subscriber_subgraph: Self::dispatch(&tracer_provider_subgraph), + subgraph_callback }) .mount(&subgraphs) .await; diff --git a/apollo-router/tests/integration/telemetry/datadog.rs b/apollo-router/tests/integration/telemetry/datadog.rs index d2d3f58d25..6aed76ff6d 100644 --- a/apollo-router/tests/integration/telemetry/datadog.rs +++ b/apollo-router/tests/integration/telemetry/datadog.rs @@ -2,13 +2,18 @@ extern crate core; use std::collections::HashMap; use std::collections::HashSet; +use std::sync::atomic::AtomicBool; use std::time::Duration; use anyhow::anyhow; +use opentelemetry_api::trace::TraceContextExt; use opentelemetry_api::trace::TraceId; use serde_json::json; use serde_json::Value; use tower::BoxError; +use tracing::Span; +use tracing_opentelemetry::OpenTelemetrySpanExt; +use wiremock::ResponseTemplate; use crate::integration::common::graph_os_enabled; use crate::integration::common::Telemetry; @@ -25,6 +30,38 @@ struct TraceSpec { unmeasured_spans: HashSet<&'static str>, } +#[tokio::test(flavor = "multi_thread")] +async fn test_no_sample() -> Result<(), BoxError> { + if !graph_os_enabled() { + return Ok(()); + } + let subgraph_was_sampled = std::sync::Arc::new(AtomicBool::new(false)); + let subgraph_was_sampled_callback = subgraph_was_sampled.clone(); + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Datadog) + .config(include_str!("fixtures/datadog_no_sample.router.yaml")) + .responder(ResponseTemplate::new(200).set_body_json( + json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), + )) + .subgraph_callback(Box::new(move || { + let sampled = Span::current().context().span().span_context().is_sampled(); + subgraph_was_sampled_callback.store(sampled, std::sync::atomic::Ordering::SeqCst); + })) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); + let (_id, result) = router.execute_untraced_query(&query).await; + router.graceful_shutdown().await; + assert!(result.status().is_success()); + assert!(!subgraph_was_sampled.load(std::sync::atomic::Ordering::SeqCst)); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn test_default_span_names() -> Result<(), BoxError> { if !graph_os_enabled() { diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_no_sample.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_no_sample.router.yaml new file mode 100644 index 0000000000..d89d104346 --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_no_sample.router.yaml @@ -0,0 +1,24 @@ +telemetry: + apollo: + field_level_instrumentation_sampler: always_off + exporters: + tracing: + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + format: datadog + common: + service_name: router + # NOT always_off to allow us to test a sampling probability of zero + sampler: 0.0 + datadog: + enabled: true + batch_processor: + scheduled_delay: 100ms + fixed_span_names: false + enable_span_mapping: false + instrumentation: + spans: + mode: spec_compliant + + From b85aaa21d980b9dd0651415415f617e89aeb60e3 Mon Sep 17 00:00:00 2001 From: Bryn Cooke Date: Mon, 16 Sep 2024 16:52:50 +0100 Subject: [PATCH 24/56] Changelog for 6005 (#6006) Co-authored-by: bryn --- .changesets/fix_bryn_datadog_sample_propagation.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changesets/fix_bryn_datadog_sample_propagation.md diff --git a/.changesets/fix_bryn_datadog_sample_propagation.md b/.changesets/fix_bryn_datadog_sample_propagation.md new file mode 100644 index 0000000000..87b2322d51 --- /dev/null +++ b/.changesets/fix_bryn_datadog_sample_propagation.md @@ -0,0 +1,7 @@ +### Fix datadog sample propagation ([PR #6005](https://github.com/apollographql/router/pull/6005)) + +#5788 introduced a regression where samping was being set on propagated headers regardless of the sampling decision in the router or upstream. + +This PR reverts the code in question and adds a test to check that a non-sampled request will not result in sampling in the downstream subgraph service. + +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/6005 From 30f3004c8c66fafd5f5bec9b5e4633d655c63858 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Mon, 16 Sep 2024 22:27:15 +0300 Subject: [PATCH 25/56] docs: Remove superfluous step from RELEASE_CHECKLIST.md (#5981) --- RELEASE_CHECKLIST.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md index e6c78bce10..4ba8c4326f 100644 --- a/RELEASE_CHECKLIST.md +++ b/RELEASE_CHECKLIST.md @@ -189,13 +189,7 @@ Start following the steps below to start a release PR. The process is **not ful git commit -m "prep release: v${APOLLO_ROUTER_RELEASE_VERSION}${APOLLO_ROUTER_PRERELEASE_SUFFIX}" ``` -9. Push this commit up to the existing release PR: - - ``` - git push "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "${APOLLO_ROUTER_RELEASE_VERSION}" - ``` - -10. Git tag the current commit and & push the branch and the pre-release tag simultaneously: +9. Git tag the current commit and & push the branch and the pre-release tag simultaneously: This process will kick off the bulk of the release process on CircleCI, including building each architecture on its own infrastructure and notarizing the macOS binary. @@ -204,7 +198,7 @@ Start following the steps below to start a release PR. The process is **not ful git push "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "${APOLLO_ROUTER_RELEASE_VERSION}" "v${APOLLO_ROUTER_RELEASE_VERSION}${APOLLO_ROUTER_PRERELEASE_SUFFIX}" ``` -11. Finally, publish the Crates from your local computer (this also needs to be moved to CI, but requires changing the release containers to be Rust-enabled and to restore the caches): +10. Finally, publish the Crates from your local computer (this also needs to be moved to CI, but requires changing the release containers to be Rust-enabled and to restore the caches): > Note: This command may appear unnecessarily specific, but it will help avoid publishing a version to Crates.io that doesn't match what you're currently releasing. (e.g., in the event that you've changed branches in another window) From b443ad0014bd20ce81d56161848074607371671d Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Tue, 17 Sep 2024 10:48:21 +0200 Subject: [PATCH 26/56] Run new query planner off of Tokio threads (#5998) --- .../src/query_planner/bridge_query_planner.rs | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 8895320cc5..f38011fb8a 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -288,20 +288,34 @@ impl PlannerMode { } Ok(success) } - PlannerMode::Rust(rust) => { - let start = Instant::now(); - - let result = operation - .as_deref() - .map(|n| Name::new(n).map_err(FederationError::from)) - .transpose() - .and_then(|operation| rust.build_query_plan(&doc.executable, operation)) - .map_err(|e| QueryPlannerError::FederationError(e.to_string())); - - let elapsed = start.elapsed().as_secs_f64(); - metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); - - let plan = result?; + PlannerMode::Rust(rust_planner) => { + let doc = doc.clone(); + let rust_planner = rust_planner.clone(); + let (plan, mut root_node) = tokio::task::spawn_blocking(move || { + let start = Instant::now(); + + let result = operation + .as_deref() + .map(|n| Name::new(n).map_err(FederationError::from)) + .transpose() + .and_then(|operation| { + rust_planner.build_query_plan(&doc.executable, operation) + }) + .map_err(|e| QueryPlannerError::FederationError(e.to_string())); + + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); + + result.map(|plan| { + let root_node = convert_root_query_plan_node(&plan); + (plan, root_node) + }) + }) + .await + .expect("query planner panicked")?; + if let Some(node) = &mut root_node { + init_query_plan_root_node(node)?; + } // Dummy value overwritten below in `BrigeQueryPlanner::plan` let usage_reporting = UsageReporting { @@ -309,11 +323,6 @@ impl PlannerMode { referenced_fields_by_type: Default::default(), }; - let mut root_node = convert_root_query_plan_node(&plan); - if let Some(node) = &mut root_node { - init_query_plan_root_node(node)?; - } - Ok(PlanSuccess { usage_reporting, data: QueryPlanResult { From bc554cedd97eec5247167d4a70bfb26132dabe9a Mon Sep 17 00:00:00 2001 From: Bryn Cooke Date: Tue, 17 Sep 2024 15:52:31 +0100 Subject: [PATCH 27/56] Fix APQ and introspection gauges (#6012) Co-authored-by: bryn --- .changesets/fix_bryn_fix_gauges.md | 6 +- apollo-router/src/introspection.rs | 26 ++++--- .../cost_calculator/static_cost.rs | 13 +++- apollo-router/src/plugins/test.rs | 14 +++- .../src/query_planner/bridge_query_planner.rs | 73 ++++++++++++++----- .../bridge_query_planner_pool.rs | 24 +++++- apollo-router/src/services/layers/apq.rs | 8 ++ apollo-router/src/services/router/service.rs | 8 ++ apollo-router/tests/common.rs | 2 +- .../telemetry/fixtures/prometheus.router.yaml | 33 +++++---- .../tests/integration/telemetry/metrics.rs | 33 +++++++++ 11 files changed, 185 insertions(+), 55 deletions(-) diff --git a/.changesets/fix_bryn_fix_gauges.md b/.changesets/fix_bryn_fix_gauges.md index 107f201010..0a33f8544f 100644 --- a/.changesets/fix_bryn_fix_gauges.md +++ b/.changesets/fix_bryn_fix_gauges.md @@ -1,4 +1,4 @@ -### Gauges stop working after hot reload ([PR #5996](https://github.com/apollographql/router/pull/5996), [PR #5999](https://github.com/apollographql/router/pull/5999)) +### Gauges stop working after hot reload ([PR #5996](https://github.com/apollographql/router/pull/5996), [PR #5999](https://github.com/apollographql/router/pull/5999), [PR #5999](https://github.com/apollographql/router/pull/6012)) When the router reloads the schema or config, some gauges stopped working. These were: * `apollo.router.cache.storage.estimated_size` @@ -9,4 +9,6 @@ When the router reloads the schema or config, some gauges stopped working. These The gauges will now continue to function after a router hot reload. -By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5996 and https://github.com/apollographql/router/pull/5999 +As a result of this change, introspection queries will now share the same cache even when query planner pooling is used. + +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5996 and https://github.com/apollographql/router/pull/5999 and https://github.com/apollographql/router/pull/6012 diff --git a/apollo-router/src/introspection.rs b/apollo-router/src/introspection.rs index e7822639ab..be0e4a1d88 100644 --- a/apollo-router/src/introspection.rs +++ b/apollo-router/src/introspection.rs @@ -14,6 +14,13 @@ use crate::query_planner::QueryPlanResult; const DEFAULT_INTROSPECTION_CACHE_CAPACITY: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(5) }; +pub(crate) async fn default_cache_storage() -> CacheStorage { + // This cannot fail as redis is not used. + CacheStorage::new(DEFAULT_INTROSPECTION_CACHE_CAPACITY, None, "introspection") + .await + .expect("failed to create cache storage") +} + /// A cache containing our well known introspection queries. pub(crate) struct Introspection { cache: CacheStorage, @@ -21,18 +28,11 @@ pub(crate) struct Introspection { } impl Introspection { - pub(crate) async fn with_capacity( + pub(crate) async fn with_cache( planner: Arc>, - capacity: NonZeroUsize, + cache: CacheStorage, ) -> Result { - Ok(Self { - cache: CacheStorage::new(capacity, None, "introspection").await?, - planner, - }) - } - - pub(crate) async fn new(planner: Arc>) -> Result { - Self::with_capacity(planner, DEFAULT_INTROSPECTION_CACHE_CAPACITY).await + Ok(Self { cache, planner }) } #[cfg(test)] @@ -40,7 +40,11 @@ impl Introspection { planner: Arc>, cache: HashMap, ) -> Result { - let this = Self::with_capacity(planner, cache.len().try_into().unwrap()).await?; + let this = Self::with_cache( + planner, + CacheStorage::new(cache.len().try_into().unwrap(), None, "introspection").await?, + ) + .await?; for (query, response) in cache.into_iter() { this.cache.insert(query, response).await; diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs index 9da40bb675..7cf8b1ba4a 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs @@ -585,6 +585,7 @@ mod tests { use tower::Service; use super::*; + use crate::introspection::default_cache_storage; use crate::query_planner::BridgeQueryPlanner; use crate::services::layers::query_analysis::ParsedDocument; use crate::services::QueryPlannerContent; @@ -671,9 +672,15 @@ mod tests { .unwrap_or_default(); let supergraph_schema = schema.supergraph_schema().clone(); - let mut planner = BridgeQueryPlanner::new(schema.into(), config.clone(), None, None) - .await - .unwrap(); + let mut planner = BridgeQueryPlanner::new( + schema.into(), + config.clone(), + None, + None, + default_cache_storage().await, + ) + .await + .unwrap(); let ctx = Context::new(); ctx.extensions() diff --git a/apollo-router/src/plugins/test.rs b/apollo-router/src/plugins/test.rs index 50ceac0110..1593523015 100644 --- a/apollo-router/src/plugins/test.rs +++ b/apollo-router/src/plugins/test.rs @@ -10,6 +10,7 @@ use tower::BoxError; use tower::ServiceBuilder; use tower_service::Service; +use crate::introspection::default_cache_storage; use crate::plugin::DynPlugin; use crate::plugin::Plugin; use crate::plugin::PluginInit; @@ -95,10 +96,15 @@ impl PluginTestHarness { let sdl = schema.raw_sdl.clone(); let supergraph = schema.supergraph_schema().clone(); let rust_planner = PlannerMode::maybe_rust(&schema, &config).unwrap(); - let planner = - BridgeQueryPlanner::new(schema.into(), Arc::new(config), None, rust_planner) - .await - .unwrap(); + let planner = BridgeQueryPlanner::new( + schema.into(), + Arc::new(config), + None, + rust_planner, + default_cache_storage().await, + ) + .await + .unwrap(); (sdl, supergraph, planner.subgraph_schemas()) } else { ( diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index f38011fb8a..3c3727a324 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -30,6 +30,7 @@ use tower::Service; use super::PlanNode; use super::QueryKey; use crate::apollo_studio_interop::generate_usage_reporting; +use crate::cache::storage::CacheStorage; use crate::configuration::IntrospectionMode as IntrospectionConfig; use crate::configuration::QueryPlannerMode; use crate::error::PlanErrors; @@ -38,6 +39,7 @@ use crate::error::SchemaError; use crate::error::ServiceBuildError; use crate::error::ValidationErrors; use crate::graphql; +use crate::graphql::Response; use crate::introspection::Introspection; use crate::json_ext::Object; use crate::json_ext::Path; @@ -243,6 +245,7 @@ impl PlannerMode { sdl: &str, configuration: &Configuration, old_js_planner: &Option>>, + cache: CacheStorage, ) -> Result, ServiceBuildError> { let js_planner = match self { Self::Js(js) => js.clone(), @@ -253,7 +256,9 @@ impl PlannerMode { Self::js_planner(sdl, configuration, old_js_planner).await? } }; - Ok(Arc::new(Introspection::new(js_planner).await?)) + Ok(Arc::new( + Introspection::with_cache(js_planner, cache).await?, + )) } async fn plan( @@ -411,6 +416,7 @@ impl BridgeQueryPlanner { configuration: Arc, old_js_planner: Option>>, rust_planner: Option>, + cache: CacheStorage, ) -> Result { let planner = PlannerMode::new(&schema, &configuration, &old_js_planner, rust_planner).await?; @@ -422,12 +428,12 @@ impl BridgeQueryPlanner { IntrospectionConfig::New => IntrospectionMode::Rust, IntrospectionConfig::Legacy => IntrospectionMode::Js( planner - .js_introspection(&schema.raw_sdl, &configuration, &old_js_planner) + .js_introspection(&schema.raw_sdl, &configuration, &old_js_planner, cache) .await?, ), IntrospectionConfig::Both => IntrospectionMode::Both( planner - .js_introspection(&schema.raw_sdl, &configuration, &old_js_planner) + .js_introspection(&schema.raw_sdl, &configuration, &old_js_planner, cache) .await?, ), } @@ -1228,6 +1234,7 @@ mod tests { use tower::ServiceExt; use super::*; + use crate::introspection::default_cache_storage; use crate::metrics::FutureMetricsExt as _; use crate::services::subgraph; use crate::services::supergraph; @@ -1272,9 +1279,15 @@ mod tests { let sdl = include_str!("../testdata/minimal_fed1_supergraph.graphql"); let config = Arc::default(); let schema = Schema::parse(sdl, &config).unwrap(); - let _planner = BridgeQueryPlanner::new(schema.into(), config, None, None) - .await - .unwrap(); + let _planner = BridgeQueryPlanner::new( + schema.into(), + config, + None, + None, + default_cache_storage().await, + ) + .await + .unwrap(); assert_gauge!( "apollo.router.supergraph.federation", @@ -1289,9 +1302,15 @@ mod tests { let sdl = include_str!("../testdata/minimal_supergraph.graphql"); let config = Arc::default(); let schema = Schema::parse(sdl, &config).unwrap(); - let _planner = BridgeQueryPlanner::new(schema.into(), config, None, None) - .await - .unwrap(); + let _planner = BridgeQueryPlanner::new( + schema.into(), + config, + None, + None, + default_cache_storage().await, + ) + .await + .unwrap(); assert_gauge!( "apollo.router.supergraph.federation", @@ -1308,9 +1327,15 @@ mod tests { let schema = Arc::new(Schema::parse(EXAMPLE_SCHEMA, &Default::default()).unwrap()); let query = include_str!("testdata/unknown_introspection_query.graphql"); - let planner = BridgeQueryPlanner::new(schema.clone(), Default::default(), None, None) - .await - .unwrap(); + let planner = BridgeQueryPlanner::new( + schema.clone(), + Default::default(), + None, + None, + default_cache_storage().await, + ) + .await + .unwrap(); let doc = Query::parse_document(query, None, &schema, &Configuration::default()).unwrap(); @@ -1408,9 +1433,15 @@ mod tests { let configuration = Arc::new(configuration); let schema = Schema::parse(EXAMPLE_SCHEMA, &configuration).unwrap(); - let planner = BridgeQueryPlanner::new(schema.into(), configuration.clone(), None, None) - .await - .unwrap(); + let planner = BridgeQueryPlanner::new( + schema.into(), + configuration.clone(), + None, + None, + default_cache_storage().await, + ) + .await + .unwrap(); macro_rules! s { ($query: expr) => { @@ -1716,9 +1747,15 @@ mod tests { let configuration = Arc::new(configuration); let schema = Schema::parse(schema, &configuration).unwrap(); - let planner = BridgeQueryPlanner::new(schema.into(), configuration.clone(), None, None) - .await - .unwrap(); + let planner = BridgeQueryPlanner::new( + schema.into(), + configuration.clone(), + None, + None, + default_cache_storage().await, + ) + .await + .unwrap(); let doc = Query::parse_document( original_query, diff --git a/apollo-router/src/query_planner/bridge_query_planner_pool.rs b/apollo-router/src/query_planner/bridge_query_planner_pool.rs index 5c4c766218..3200e59e7e 100644 --- a/apollo-router/src/query_planner/bridge_query_planner_pool.rs +++ b/apollo-router/src/query_planner/bridge_query_planner_pool.rs @@ -21,8 +21,11 @@ use tower::ServiceExt; use super::bridge_query_planner::BridgeQueryPlanner; use super::QueryPlanResult; +use crate::cache::storage::CacheStorage; use crate::error::QueryPlannerError; use crate::error::ServiceBuildError; +use crate::graphql::Response; +use crate::introspection::default_cache_storage; use crate::metrics::meter_provider; use crate::query_planner::PlannerMode; use crate::services::QueryPlannerRequest; @@ -46,6 +49,7 @@ pub(crate) struct BridgeQueryPlannerPool { v8_heap_used_gauge: Arc>>>, v8_heap_total: Arc, v8_heap_total_gauge: Arc>>>, + introspection_cache: CacheStorage, } impl BridgeQueryPlannerPool { @@ -66,16 +70,28 @@ impl BridgeQueryPlannerPool { let mut old_js_planners_iterator = old_js_planners.into_iter(); - (0..size.into()).for_each(|_| { + // All query planners in the pool now share the same introspection cache. + // This allows meaningful gauges, and it makes sense that queries should be cached across all planners. + let introspection_cache = default_cache_storage().await; + + for _ in 0..size.into() { let schema = schema.clone(); let configuration = configuration.clone(); let rust_planner = rust_planner.clone(); + let introspection_cache = introspection_cache.clone(); let old_planner = old_js_planners_iterator.next(); join_set.spawn(async move { - BridgeQueryPlanner::new(schema, configuration, old_planner, rust_planner).await + BridgeQueryPlanner::new( + schema, + configuration, + old_planner, + rust_planner, + introspection_cache, + ) + .await }); - }); + } let mut bridge_query_planners = Vec::new(); @@ -142,6 +158,7 @@ impl BridgeQueryPlannerPool { v8_heap_used_gauge: Default::default(), v8_heap_total, v8_heap_total_gauge: Default::default(), + introspection_cache, }) } @@ -218,6 +235,7 @@ impl BridgeQueryPlannerPool { Some(self.create_heap_used_gauge()); *self.v8_heap_total_gauge.lock().expect("lock poisoned") = Some(self.create_heap_total_gauge()); + self.introspection_cache.activate(); } } diff --git a/apollo-router/src/services/layers/apq.rs b/apollo-router/src/services/layers/apq.rs index 6912c5e28c..d4f1119161 100644 --- a/apollo-router/src/services/layers/apq.rs +++ b/apollo-router/src/services/layers/apq.rs @@ -54,6 +54,14 @@ pub(crate) struct APQLayer { cache: Option>, } +impl APQLayer { + pub(crate) fn activate(&self) { + if let Some(cache) = &self.cache { + cache.activate(); + } + } +} + impl APQLayer { pub(crate) fn with_cache(cache: DeduplicatingCache) -> Self { Self { cache: Some(cache) } diff --git a/apollo-router/src/services/router/service.rs b/apollo-router/src/services/router/service.rs index f5013e5380..9e379ae3c4 100644 --- a/apollo-router/src/services/router/service.rs +++ b/apollo-router/src/services/router/service.rs @@ -873,6 +873,14 @@ impl RouterCreator { } else { APQLayer::disabled() }; + // There is a problem here. + // APQ isn't a plugin and so cannot participate in plugin lifecycle events. + // After telemetry `activate` NO part of the pipeline can fail as globals have been interacted with. + // However, the APQLayer uses DeduplicatingCache which is fallible. So if this fails on hot reload the router will be + // left in an inconsistent state and all metrics will likely stop working. + // Fixing this will require a larger refactor to bring APQ into the router lifecycle. + // For now just call activate to make the gauges work on the happy path. + apq_layer.activate(); Ok(Self { supergraph_creator, diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index 42eb5d3c9a..664f8970a6 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -523,7 +523,7 @@ impl IntegrationTest { pub fn execute_huge_query( &self, ) -> impl std::future::Future { - self.execute_query_internal(&json!({"query":"query {topProducts{name, name, name, name, name, name, name, name, name, name}}","variables":{}}), None, None) + self.execute_query_internal(&json!({"query":"query {topProducts{name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name}}","variables":{}}), None, None) } #[allow(dead_code)] diff --git a/apollo-router/tests/integration/telemetry/fixtures/prometheus.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/prometheus.router.yaml index 53c258bb41..1ae02a4dff 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/prometheus.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/prometheus.router.yaml @@ -1,5 +1,5 @@ limits: - http_max_request_bytes: 60 + http_max_request_bytes: 200 telemetry: exporters: metrics: @@ -9,18 +9,18 @@ telemetry: path: /metrics common: views: - - name: apollo_router_http_request_duration_seconds - aggregation: - histogram: - buckets: - - 0.1 - - 0.5 - - 1 - - 2 - - 3 - - 4 - - 5 - - 100 + - name: apollo_router_http_request_duration_seconds + aggregation: + histogram: + buckets: + - 0.1 + - 0.5 + - 1 + - 2 + - 3 + - 4 + - 5 + - 100 attributes: subgraph: all: @@ -39,3 +39,10 @@ override_subgraph_url: products: http://localhost:4005 include_subgraph_errors: all: true +supergraph: + introspection: true +apq: + router: + cache: + in_memory: + limit: 1000 \ No newline at end of file diff --git a/apollo-router/tests/integration/telemetry/metrics.rs b/apollo-router/tests/integration/telemetry/metrics.rs index cb317e2cc9..aee5de813e 100644 --- a/apollo-router/tests/integration/telemetry/metrics.rs +++ b/apollo-router/tests/integration/telemetry/metrics.rs @@ -233,7 +233,22 @@ async fn test_gauges_on_reload() { router.execute_default_query().await; router.update_config(PROMETHEUS_CONFIG).await; router.assert_reloaded().await; + + // Regular query router.execute_default_query().await; + + // Introspection query + router + .execute_query(&json!({"query":"{__schema {types {name}}}","variables":{}})) + .await; + + // Persisted query + router + .execute_query( + &json!({"query": "{__typename}", "variables":{}, "extensions": {"persistedQuery":{"version" : 1, "sha256Hash" : "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}}) + ) + .await; + router .assert_metrics_contains(r#"apollo_router_cache_storage_estimated_size{kind="query planner",type="memory",otel_scope_name="apollo/router"} "#, None) .await; @@ -255,4 +270,22 @@ async fn test_gauges_on_reload() { None, ) .await; + router + .assert_metrics_contains( + r#"apollo_router_cache_size{kind="APQ",type="memory",otel_scope_name="apollo/router"} 1"#, + None, + ) + .await; + router + .assert_metrics_contains( + r#"apollo_router_cache_size{kind="query planner",type="memory",otel_scope_name="apollo/router"} 3"#, + None, + ) + .await; + router + .assert_metrics_contains( + r#"apollo_router_cache_size{kind="introspection",type="memory",otel_scope_name="apollo/router"} 1"#, + None, + ) + .await; } From 5ace1e4df556eee31873a14f4a5548b6f4b45e66 Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Tue, 17 Sep 2024 20:32:17 -0700 Subject: [PATCH 28/56] Fix bugs identified by interface object tests (#6019) This PR: - Fixes bugs identified by failing tests in `interface_object.rs`. - Fixes a test bug in `can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation()`, which was asserting the incorrect result for plan rewrites ([JS for comparison](https://github.com/apollographql/federation/blob/%40apollo/query-planner%402.7.8/query-planner-js/src/__tests__/buildPlan.interfaceObject.test.ts#L334)). - Adjusts a snapshot assertion in the test `it_handles_interface_object_input_rewrites_when_cloning_dependency_graph()` to account for traversal ordering differences with JS. (The parallel node order is different, and `S3` is replaced with `S4` due to the subgraphs being equivalent/plan costs being equal.) - Unrelated nit, but fixes some pluralization in variable naming. After this PR, all `interface_object.rs` tests are passing. --- .../src/query_graph/graph_path.rs | 14 +++--- .../src/query_plan/fetch_dependency_graph.rs | 2 +- .../interface_object.rs | 48 ++++++++----------- 3 files changed, 28 insertions(+), 36 deletions(-) diff --git a/apollo-federation/src/query_graph/graph_path.rs b/apollo-federation/src/query_graph/graph_path.rs index b9486f8434..c588f39d7c 100644 --- a/apollo-federation/src/query_graph/graph_path.rs +++ b/apollo-federation/src/query_graph/graph_path.rs @@ -2169,7 +2169,7 @@ impl OpGraphPath { let Some(self_edge) = self.edges[diff_pos] else { return Ok(false); }; - let Some(other_edge) = self.edges[diff_pos] else { + let Some(other_edge) = other.edges[diff_pos] else { return Ok(false); }; let other_edge_weight = other.graph.edge_weight(other_edge)?; @@ -2190,7 +2190,7 @@ impl OpGraphPath { // from the interface while the other one from an implementation, they won't be technically // the "same" edge index. So we check that both are key-resolution edges, to the same // subgraph and type, and with the same condition. - let Some(other_next_edge) = self.edges[diff_pos + 1] else { + let Some(other_next_edge) = other.edges[diff_pos + 1] else { return Ok(false); }; let (_, self_edge_tail) = other.graph.edge_endpoints(self_edge)?; @@ -3429,13 +3429,13 @@ impl SimultaneousPathsWithLazyIndirectPaths { "{} indirect paths", paths_with_non_collecting_edges.paths.len() ); - for paths_with_non_collecting_edges in + for path_with_non_collecting_edges in paths_with_non_collecting_edges.paths.iter() { - debug!("For indirect path {paths_with_non_collecting_edges}:"); + debug!("For indirect path {path_with_non_collecting_edges}:"); let span = debug_span!(" |"); let _gaurd = span.enter(); - let (advance_options, _) = paths_with_non_collecting_edges + let (advance_options, _) = path_with_non_collecting_edges .advance_with_operation_element( supergraph_schema.clone(), operation_element, @@ -3458,7 +3458,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { if advance_options.is_empty() { return Err(FederationError::internal(format!( "Unexpected empty options after non-collecting path {} for {}", - paths_with_non_collecting_edges, operation_element, + path_with_non_collecting_edges, operation_element, ))); } // There is a special case we can deal with now. Namely, suppose we have a @@ -3485,7 +3485,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { // the new path renders unnecessary. Do note that we only make that check // when the new option is a single-path option, because this gets kind of // complicated otherwise. - if paths_with_non_collecting_edges.tail_is_interface_object()? { + if path_with_non_collecting_edges.tail_is_interface_object()? { for indirect_option in &advance_options { if indirect_option.0.len() == 1 { let mut new_options = vec![]; diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index 17767c1668..ad2134aaff 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -1493,7 +1493,7 @@ impl FetchDependencyGraph { input_selection_set.contains(sub_selection_set) })) } else if !implementation_input_selections.is_empty() { - Ok(interface_input_selections.iter().all(|input| { + Ok(implementation_input_selections.iter().all(|input| { let Some(input_selection_set) = input.selection_set() else { return false; }; diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs index 5a99022d4a..0cd50ce299 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs @@ -40,8 +40,6 @@ const SUBGRAPH2: &str = r#" "#; #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure (fetch node for `iFromS1.y` is missing) fn can_use_a_key_on_an_interface_object_type() { let planner = planner!( S1: SUBGRAPH1, @@ -223,8 +221,6 @@ fn does_not_rely_on_an_interface_object_directly_for_typename() { } #[test] -#[should_panic(expected = r#"snapshot assertion"#)] -// TODO: investigate this failure (missing fetch node for `iFromS2 { ... on I { y } }`) fn does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is_requested() { let planner = planner!( S1: SUBGRAPH1, @@ -294,8 +290,6 @@ fn does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is } #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure (fetch node for `iFromS1.y` is missing) fn can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation() { let planner = planner!( S1: SUBGRAPH1, @@ -354,13 +348,19 @@ fn can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation( let rewrite = rewrites[0].clone(); match rewrite.deref() { FetchDataRewrite::ValueSetter(v) => { - assert_eq!(v.path.len(), 1); + assert_eq!(v.path.len(), 2); match &v.path[0] { FetchDataPathElement::TypenameEquals(typename) => { assert_eq!(*typename, apollo_compiler::name!("A")) } _ => unreachable!("Expected FetchDataPathElement::TypenameEquals path"), } + match &v.path[1] { + FetchDataPathElement::Key(name, _conditions) => { + assert_eq!(*name, apollo_compiler::name!("__typename")) + } + _ => unreachable!("Expected FetchDataPathElement::Key path"), + } assert_eq!(v.set_value_to, "I"); } _ => unreachable!("Expected FetchDataRewrite::ValueSetter rewrite"), @@ -423,8 +423,6 @@ fn handles_query_of_an_interface_field_for_a_specific_implementation_when_query_ } #[test] -#[should_panic(expected = r#"snapshot assertion"#)] -// TODO: investigate this failure (missing fetch node for "everything.@ { ... on I { expansiveField } }") fn it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_with_lists() { let planner = planner!( S1: r#" @@ -517,8 +515,6 @@ fn it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_wit } #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure (missing fetch nodes for i.x and i.y) fn it_handles_requires_on_concrete_type_of_field_provided_by_interface_object() { let planner = planner!( S1: r#" @@ -613,8 +609,6 @@ fn it_handles_requires_on_concrete_type_of_field_provided_by_interface_object() } #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure (missing fetch node for `i.t.relatedIs.id`) fn it_handles_interface_object_in_nested_entity() { let planner = planner!( S1: r#" @@ -716,8 +710,6 @@ fn it_handles_interface_object_in_nested_entity() { } #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure fn it_handles_interface_object_input_rewrites_when_cloning_dependency_graph() { let planner = planner!( S1: r#" @@ -797,33 +789,33 @@ fn it_handles_interface_object_input_rewrites_when_cloning_dependency_graph() { } }, Parallel { - Flatten(path: "i") { - Fetch(service: "S2") { + Flatten(path: "i.i2") { + Fetch(service: "S4") { { - ... on I { + ... on T { __typename - i1 + t1 } } => { - ... on I { - i3 + ... on T { + __typename + t2 } } }, }, - Flatten(path: "i.i2") { - Fetch(service: "S3") { + Flatten(path: "i") { + Fetch(service: "S2") { { - ... on T { + ... on I { __typename - t1 + i1 } } => { - ... on T { - __typename - t2 + ... on I { + i3 } } }, From 3436380199d4c6f5427964603e9c33f82d1144fb Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Wed, 18 Sep 2024 07:07:15 +0200 Subject: [PATCH 29/56] Fix introspection bug, run off Tokio threads, enable "both" mode (#6014) * Update apollo-compiler to fix ROUTER-703 introspection incorrectly listing unused built-in scalars: https://github.com/apollographql/apollo-rs/pull/911 * Run Rust introspection off of Tokio threads, with the same reasoning as for the query planner in https://github.com/apollographql/router/pull/5998 * Enable introspection "both" mode by default. The legacy implementation is still used in the response but both are run and their results compared. The result of comparing both responses is logged at `DEBUG` level. At `TRACE` level, mismatches print the full query and the response diff. Example usage: `router --log error,apollo_router=info,apollo_router::query_planner::dual_introspection=trace` --- .changesets/config_introspection_both.md | 14 ++ Cargo.lock | 8 +- Cargo.toml | 4 +- apollo-router/src/configuration/mod.rs | 2 +- .../src/query_planner/bridge_query_planner.rs | 201 ++++-------------- .../src/query_planner/dual_introspection.rs | 137 ++++++++++++ apollo-router/src/query_planner/mod.rs | 1 + .../tests/integration/introspection.rs | 6 +- examples/supergraph-sdl/rust/Cargo.toml | 2 +- 9 files changed, 204 insertions(+), 171 deletions(-) create mode 100644 .changesets/config_introspection_both.md create mode 100644 apollo-router/src/query_planner/dual_introspection.rs diff --git a/.changesets/config_introspection_both.md b/.changesets/config_introspection_both.md new file mode 100644 index 0000000000..036cdfe83f --- /dev/null +++ b/.changesets/config_introspection_both.md @@ -0,0 +1,14 @@ +### Enable both introspection implementation by default ([PR #6014](https://github.com/apollographql/router/pull/6014)) + +As part of the process to replace JavaScript schema introspection with a more performant Rust implementation in the router, we are enabling the router to run both implementations as a default. This allows us to definitively assess reliability and stability of Rust implementation before completely removing JavaScript one. As before, it's possible to toggle between implementations using the `experimental_introspection_mode` config key. Possible values are: `new` (runs only Rust-based validation), `legacy` (runs only JS-based validation), `both` (runs both in comparison, logging errors if a difference arises). + +The `both` mode is now the default, which will result in **no client-facing impact** but will record the metrics for the outcome of comparison as a `apollo.router.operations.introspection.both` counter. If this counter in your metrics has `rust_error = true` or `is_matched = false`, please open an issue. + +Schema introspection itself is disabled by default, so the above has no effect unless it is enabled in configuration: + +```yaml +supergraph: + introspection: true +``` + +By [@SimonSapin](https://github.com/SimonSapin) in https://github.com/apollographql/router/pull/6014 diff --git a/Cargo.lock b/Cargo.lock index b27f7900e9..cefb7bcb6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,9 +159,9 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "apollo-compiler" -version = "1.0.0-beta.22" +version = "1.0.0-beta.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6feccaf8fab00732a73dd3a40e4aaa7a1106ceef3c0bfbc591c4e0ba27e07774" +checksum = "875f39060728ac3e775fc3fe5421225d6df92c4d5155a9524cdb198f05006d36" dependencies = [ "ahash", "apollo-parser", @@ -449,9 +449,9 @@ dependencies = [ [[package]] name = "apollo-smith" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10e88b295221ae9d2af63d5eac86cb1f47ec8449e7440253d7772fc305a6c200" +checksum = "40cff1a5989a471714cfdf53f24d0948b7f77631ab3dbd25b2f6eacbf58e5261" dependencies = [ "apollo-compiler", "apollo-parser", diff --git a/Cargo.toml b/Cargo.toml index 7672f73804..0352205ebb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,9 +49,9 @@ debug = 1 # Dependencies used in more than one place are specified here in order to keep versions in sync: # https://doc.rust-lang.org/cargo/reference/workspaces.html#the-dependencies-table [workspace.dependencies] -apollo-compiler = "=1.0.0-beta.22" +apollo-compiler = "=1.0.0-beta.23" apollo-parser = "0.8.0" -apollo-smith = "0.12.0" +apollo-smith = "0.13.0" async-trait = "0.1.77" hex = { version = "0.4.3", features = ["serde"] } http = "0.2.11" diff --git a/apollo-router/src/configuration/mod.rs b/apollo-router/src/configuration/mod.rs index 7837d8e4dc..83d0e44a1e 100644 --- a/apollo-router/src/configuration/mod.rs +++ b/apollo-router/src/configuration/mod.rs @@ -239,10 +239,10 @@ pub(crate) enum IntrospectionMode { /// Use the new Rust-based implementation. New, /// Use the old JavaScript-based implementation. - #[default] Legacy, /// Use Rust-based and Javascript-based implementations side by side, /// logging warnings if the implementations disagree. + #[default] Both, } diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 3c3727a324..24ad85ee17 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -1,6 +1,5 @@ //! Calls out to nodejs query planner -use std::cmp::Ordering; use std::collections::HashMap; use std::fmt::Debug; use std::fmt::Write; @@ -533,9 +532,15 @@ impl BridgeQueryPlanner { match &self.introspection { IntrospectionMode::Disabled => return Ok(QueryPlannerContent::IntrospectionDisabled), IntrospectionMode::Rust => { - return Ok(QueryPlannerContent::Response { - response: Box::new(self.rust_introspection(&key, &doc)?), - }); + let schema = self.schema.clone(); + let response = Box::new( + tokio::task::spawn_blocking(move || { + Self::rust_introspection(&schema, &key, &doc) + }) + .await + .expect("Introspection panicked")?, + ); + return Ok(QueryPlannerContent::Response { response }); } IntrospectionMode::Js(_) | IntrospectionMode::Both(_) => {} } @@ -561,30 +566,40 @@ impl BridgeQueryPlanner { .await .map_err(QueryPlannerError::Introspection)?, IntrospectionMode::Both(js) => { - let rust_result = match self.rust_introspection(&key, &doc) { - Ok(response) => { - if response.errors.is_empty() { - Ok(response) - } else { - Err(QueryPlannerError::Introspection(IntrospectionError { - message: Some( - response - .errors - .into_iter() - .map(|e| e.to_string()) - .collect::>() - .join(", "), - ), - })) - } - } - Err(e) => Err(e), - }; let js_result = js - .execute(key.filtered_query) + .execute(key.filtered_query.clone()) .await .map_err(QueryPlannerError::Introspection); - self.compare_introspection_responses(js_result.clone(), rust_result); + let schema = self.schema.clone(); + let js_result_clone = js_result.clone(); + tokio::task::spawn_blocking(move || { + let rust_result = match Self::rust_introspection(&schema, &key, &doc) { + Ok(response) => { + if response.errors.is_empty() { + Ok(response) + } else { + Err(QueryPlannerError::Introspection(IntrospectionError { + message: Some( + response + .errors + .into_iter() + .map(|e| e.to_string()) + .collect::>() + .join(", "), + ), + })) + } + } + Err(e) => Err(e), + }; + super::dual_introspection::compare_introspection_responses( + &key.original_query, + js_result_clone, + rust_result, + ); + }) + .await + .expect("Introspection comparison panicked"); js_result? } }; @@ -594,11 +609,11 @@ impl BridgeQueryPlanner { } fn rust_introspection( - &self, + schema: &Schema, key: &QueryKey, doc: &ParsedDocument, ) -> Result { - let schema = self.schema.api_schema(); + let schema = schema.api_schema(); let operation = doc.get_operation(key.operation_name.as_deref())?; let variable_values = Default::default(); let variable_values = @@ -621,138 +636,6 @@ impl BridgeQueryPlanner { Ok(response.into()) } - fn compare_introspection_responses( - &self, - mut js_result: Result, - mut rust_result: Result, - ) { - let is_matched; - match (&mut js_result, &mut rust_result) { - (Err(_), Err(_)) => { - is_matched = true; - } - (Err(err), Ok(_)) => { - is_matched = false; - tracing::warn!("JS introspection error: {err}") - } - (Ok(_), Err(err)) => { - is_matched = false; - tracing::warn!("Rust introspection error: {err}") - } - (Ok(js_response), Ok(rust_response)) => { - if let (Some(js_data), Some(rust_data)) = - (&mut js_response.data, &mut rust_response.data) - { - json_sort_arrays(js_data); - json_sort_arrays(rust_data); - } - is_matched = js_response.data == rust_response.data; - if is_matched { - tracing::debug!("Introspection match! 🎉") - } else { - tracing::debug!("Introspection mismatch"); - tracing::trace!("Introspection diff:\n{}", { - let rust = rust_response - .data - .as_ref() - .map(|d| serde_json::to_string_pretty(&d).unwrap()) - .unwrap_or_default(); - let js = js_response - .data - .as_ref() - .map(|d| serde_json::to_string_pretty(&d).unwrap()) - .unwrap_or_default(); - let diff = similar::TextDiff::from_lines(&js, &rust); - diff.unified_diff() - .context_radius(10) - .header("JS", "Rust") - .to_string() - }) - } - } - } - - u64_counter!( - "apollo.router.operations.introspection.both", - "Comparing JS v.s. Rust introspection", - 1, - "generation.is_matched" = is_matched, - "generation.js_error" = js_result.is_err(), - "generation.rust_error" = rust_result.is_err() - ); - - fn json_sort_arrays(value: &mut Value) { - match value { - Value::Array(array) => { - for item in array.iter_mut() { - json_sort_arrays(item) - } - array.sort_by(json_compare) - } - Value::Object(object) => { - for (_key, value) in object { - json_sort_arrays(value) - } - } - Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {} - } - } - - fn json_compare(a: &Value, b: &Value) -> Ordering { - match (a, b) { - (Value::Null, Value::Null) => Ordering::Equal, - (Value::Bool(a), Value::Bool(b)) => a.cmp(b), - (Value::Number(a), Value::Number(b)) => { - a.as_f64().unwrap().total_cmp(&b.as_f64().unwrap()) - } - (Value::String(a), Value::String(b)) => a.cmp(b), - (Value::Array(a), Value::Array(b)) => iter_cmp(a, b, json_compare), - (Value::Object(a), Value::Object(b)) => { - iter_cmp(a, b, |(key_a, a), (key_b, b)| { - debug_assert_eq!(key_a, key_b); // Response object keys are in selection set order - json_compare(a, b) - }) - } - _ => json_discriminant(a).cmp(&json_discriminant(b)), - } - } - - // TODO: use `Iterator::cmp_by` when available: - // https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.cmp_by - // https://github.com/rust-lang/rust/issues/64295 - fn iter_cmp( - a: impl IntoIterator, - b: impl IntoIterator, - cmp: impl Fn(T, T) -> Ordering, - ) -> Ordering { - use itertools::Itertools; - for either_or_both in a.into_iter().zip_longest(b) { - match either_or_both { - itertools::EitherOrBoth::Both(a, b) => { - let ordering = cmp(a, b); - if ordering != Ordering::Equal { - return ordering; - } - } - itertools::EitherOrBoth::Left(_) => return Ordering::Less, - itertools::EitherOrBoth::Right(_) => return Ordering::Greater, - } - } - Ordering::Equal - } - - fn json_discriminant(value: &Value) -> u8 { - match value { - Value::Null => 0, - Value::Bool(_) => 1, - Value::Number(_) => 2, - Value::String(_) => 3, - Value::Array(_) => 4, - Value::Object(_) => 5, - } - } - } - #[allow(clippy::too_many_arguments)] async fn plan( &self, diff --git a/apollo-router/src/query_planner/dual_introspection.rs b/apollo-router/src/query_planner/dual_introspection.rs new file mode 100644 index 0000000000..a6c51d7f63 --- /dev/null +++ b/apollo-router/src/query_planner/dual_introspection.rs @@ -0,0 +1,137 @@ +use std::cmp::Ordering; + +use serde_json_bytes::Value; + +use crate::error::QueryPlannerError; +use crate::graphql; + +pub(crate) fn compare_introspection_responses( + query: &str, + mut js_result: Result, + mut rust_result: Result, +) { + let is_matched; + match (&mut js_result, &mut rust_result) { + (Err(_), Err(_)) => { + is_matched = true; + } + (Err(err), Ok(_)) => { + is_matched = false; + tracing::warn!("JS introspection error: {err}") + } + (Ok(_), Err(err)) => { + is_matched = false; + tracing::warn!("Rust introspection error: {err}") + } + (Ok(js_response), Ok(rust_response)) => { + if let (Some(js_data), Some(rust_data)) = + (&mut js_response.data, &mut rust_response.data) + { + json_sort_arrays(js_data); + json_sort_arrays(rust_data); + } + is_matched = js_response.data == rust_response.data; + if is_matched { + tracing::debug!("Introspection match! 🎉") + } else { + tracing::debug!("Introspection mismatch"); + tracing::trace!("Introspection query:\n{query}"); + tracing::trace!("Introspection diff:\n{}", { + let rust = rust_response + .data + .as_ref() + .map(|d| serde_json::to_string_pretty(&d).unwrap()) + .unwrap_or_default(); + let js = js_response + .data + .as_ref() + .map(|d| serde_json::to_string_pretty(&d).unwrap()) + .unwrap_or_default(); + let diff = similar::TextDiff::from_lines(&js, &rust); + diff.unified_diff() + .context_radius(10) + .header("JS", "Rust") + .to_string() + }) + } + } + } + + u64_counter!( + "apollo.router.operations.introspection.both", + "Comparing JS v.s. Rust introspection", + 1, + "generation.is_matched" = is_matched, + "generation.js_error" = js_result.is_err(), + "generation.rust_error" = rust_result.is_err() + ); +} + +fn json_sort_arrays(value: &mut Value) { + match value { + Value::Array(array) => { + for item in array.iter_mut() { + json_sort_arrays(item) + } + array.sort_by(json_compare) + } + Value::Object(object) => { + for (_key, value) in object { + json_sort_arrays(value) + } + } + Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {} + } +} + +fn json_compare(a: &Value, b: &Value) -> Ordering { + match (a, b) { + (Value::Null, Value::Null) => Ordering::Equal, + (Value::Bool(a), Value::Bool(b)) => a.cmp(b), + (Value::Number(a), Value::Number(b)) => a.as_f64().unwrap().total_cmp(&b.as_f64().unwrap()), + (Value::String(a), Value::String(b)) => a.cmp(b), + (Value::Array(a), Value::Array(b)) => iter_cmp(a, b, json_compare), + (Value::Object(a), Value::Object(b)) => { + iter_cmp(a, b, |(key_a, a), (key_b, b)| { + debug_assert_eq!(key_a, key_b); // Response object keys are in selection set order + json_compare(a, b) + }) + } + _ => json_discriminant(a).cmp(&json_discriminant(b)), + } +} + +// TODO: use `Iterator::cmp_by` when available: +// https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.cmp_by +// https://github.com/rust-lang/rust/issues/64295 +fn iter_cmp( + a: impl IntoIterator, + b: impl IntoIterator, + cmp: impl Fn(T, T) -> Ordering, +) -> Ordering { + use itertools::Itertools; + for either_or_both in a.into_iter().zip_longest(b) { + match either_or_both { + itertools::EitherOrBoth::Both(a, b) => { + let ordering = cmp(a, b); + if ordering != Ordering::Equal { + return ordering; + } + } + itertools::EitherOrBoth::Left(_) => return Ordering::Less, + itertools::EitherOrBoth::Right(_) => return Ordering::Greater, + } + } + Ordering::Equal +} + +fn json_discriminant(value: &Value) -> u8 { + match value { + Value::Null => 0, + Value::Bool(_) => 1, + Value::Number(_) => 2, + Value::String(_) => 3, + Value::Array(_) => 4, + Value::Object(_) => 5, + } +} diff --git a/apollo-router/src/query_planner/mod.rs b/apollo-router/src/query_planner/mod.rs index ad23fbb80e..bff26390e9 100644 --- a/apollo-router/src/query_planner/mod.rs +++ b/apollo-router/src/query_planner/mod.rs @@ -14,6 +14,7 @@ pub(crate) mod bridge_query_planner; mod bridge_query_planner_pool; mod caching_query_planner; mod convert; +mod dual_introspection; pub(crate) mod dual_query_planner; mod execution; pub(crate) mod fetch; diff --git a/apollo-router/tests/integration/introspection.rs b/apollo-router/tests/integration/introspection.rs index 38fe62a70d..64aea564d5 100644 --- a/apollo-router/tests/integration/introspection.rs +++ b/apollo-router/tests/integration/introspection.rs @@ -233,7 +233,7 @@ async fn both_mode_integration() { let mut router = IntegrationTest::builder() .config( " - experimental_introspection_mode: both + # `experimental_introspection_mode` now defaults to `both` supergraph: introspection: true ", @@ -249,8 +249,6 @@ async fn both_mode_integration() { "query": include_str!("../fixtures/introspect_full_schema.graphql"), })) .await; - // TODO: should be a match after https://apollographql.atlassian.net/browse/ROUTER-703 - // router.assert_log_contains("Introspection match! 🎉").await; - router.assert_log_contains("Introspection mismatch").await; + router.assert_log_contains("Introspection match! 🎉").await; router.graceful_shutdown().await; } diff --git a/examples/supergraph-sdl/rust/Cargo.toml b/examples/supergraph-sdl/rust/Cargo.toml index 97d3d9d67f..ead321ca05 100644 --- a/examples/supergraph-sdl/rust/Cargo.toml +++ b/examples/supergraph-sdl/rust/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] anyhow = "1" -apollo-compiler = "=1.0.0-beta.22" +apollo-compiler = "=1.0.0-beta.23" apollo-router = { path = "../../../apollo-router" } async-trait = "0.1" tower = { version = "0.4", features = ["full"] } From 3cde6c0c2caf1e6b925bf8f4231662ddd5e94c42 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 18 Sep 2024 14:55:41 +0300 Subject: [PATCH 30/56] prep release: v1.55.0-rc.0 --- Cargo.lock | 8 +- apollo-federation/Cargo.toml | 2 +- apollo-router-benchmarks/Cargo.toml | 2 +- apollo-router-scaffold/Cargo.toml | 2 +- .../templates/base/Cargo.template.toml | 2 +- .../templates/base/xtask/Cargo.template.toml | 2 +- apollo-router/Cargo.toml | 4 +- .../tracing/docker-compose.datadog.yml | 2 +- dockerfiles/tracing/docker-compose.jaeger.yml | 2 +- dockerfiles/tracing/docker-compose.zipkin.yml | 2 +- helm/chart/router/Chart.yaml | 4 +- helm/chart/router/README.md | 6 +- licenses.html | 807 +++++++++++------- scripts/install.sh | 2 +- 14 files changed, 524 insertions(+), 323 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cefb7bcb6a..385b8a5a5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,7 +178,7 @@ dependencies = [ [[package]] name = "apollo-federation" -version = "1.54.0" +version = "1.55.0-rc.0" dependencies = [ "apollo-compiler", "derive_more", @@ -229,7 +229,7 @@ dependencies = [ [[package]] name = "apollo-router" -version = "1.54.0" +version = "1.55.0-rc.0" dependencies = [ "access-json", "ahash", @@ -401,7 +401,7 @@ dependencies = [ [[package]] name = "apollo-router-benchmarks" -version = "1.54.0" +version = "1.55.0-rc.0" dependencies = [ "apollo-parser", "apollo-router", @@ -417,7 +417,7 @@ dependencies = [ [[package]] name = "apollo-router-scaffold" -version = "1.54.0" +version = "1.55.0-rc.0" dependencies = [ "anyhow", "cargo-scaffold", diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml index c29f03a54a..2f11c95349 100644 --- a/apollo-federation/Cargo.toml +++ b/apollo-federation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-federation" -version = "1.54.0" +version = "1.55.0-rc.0" authors = ["The Apollo GraphQL Contributors"] edition = "2021" description = "Apollo Federation" diff --git a/apollo-router-benchmarks/Cargo.toml b/apollo-router-benchmarks/Cargo.toml index 6b8d060d88..632fe52e8d 100644 --- a/apollo-router-benchmarks/Cargo.toml +++ b/apollo-router-benchmarks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-benchmarks" -version = "1.54.0" +version = "1.55.0-rc.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/Cargo.toml b/apollo-router-scaffold/Cargo.toml index 4c47e46b94..e02093dd24 100644 --- a/apollo-router-scaffold/Cargo.toml +++ b/apollo-router-scaffold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-scaffold" -version = "1.54.0" +version = "1.55.0-rc.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/templates/base/Cargo.template.toml b/apollo-router-scaffold/templates/base/Cargo.template.toml index 290952c5a2..9b38fbb051 100644 --- a/apollo-router-scaffold/templates/base/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/Cargo.template.toml @@ -22,7 +22,7 @@ apollo-router = { path ="{{integration_test}}apollo-router" } apollo-router = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} # Note if you update these dependencies then also update xtask/Cargo.toml -apollo-router = "1.54.0" +apollo-router = "1.55.0-rc.0" {{/if}} {{/if}} async-trait = "0.1.52" diff --git a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml index 9c9a956892..46d2c1ede9 100644 --- a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml @@ -13,7 +13,7 @@ apollo-router-scaffold = { path ="{{integration_test}}apollo-router-scaffold" } {{#if branch}} apollo-router-scaffold = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} -apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.54.0" } +apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.55.0-rc.0" } {{/if}} {{/if}} anyhow = "1.0.58" diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 00c0da7a66..51b6bd93ca 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router" -version = "1.54.0" +version = "1.55.0-rc.0" authors = ["Apollo Graph, Inc. "] repository = "https://github.com/apollographql/router/" documentation = "https://docs.rs/apollo-router" @@ -68,7 +68,7 @@ askama = "0.12.1" access-json = "0.1.0" anyhow = "1.0.86" apollo-compiler.workspace = true -apollo-federation = { path = "../apollo-federation", version = "=1.54.0" } +apollo-federation = { path = "../apollo-federation", version = "=1.55.0-rc.0" } arc-swap = "1.6.0" async-channel = "1.9.0" async-compression = { version = "0.4.6", features = [ diff --git a/dockerfiles/tracing/docker-compose.datadog.yml b/dockerfiles/tracing/docker-compose.datadog.yml index fc25cbba4f..2523f55e50 100644 --- a/dockerfiles/tracing/docker-compose.datadog.yml +++ b/dockerfiles/tracing/docker-compose.datadog.yml @@ -3,7 +3,7 @@ services: apollo-router: container_name: apollo-router - image: ghcr.io/apollographql/router:v1.54.0 + image: ghcr.io/apollographql/router:v1.55.0-rc.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/datadog.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.jaeger.yml b/dockerfiles/tracing/docker-compose.jaeger.yml index fd27332b78..56537704c9 100644 --- a/dockerfiles/tracing/docker-compose.jaeger.yml +++ b/dockerfiles/tracing/docker-compose.jaeger.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router #build: ./router - image: ghcr.io/apollographql/router:v1.54.0 + image: ghcr.io/apollographql/router:v1.55.0-rc.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/jaeger.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml index 4129159c1b..7efe56a19d 100644 --- a/dockerfiles/tracing/docker-compose.zipkin.yml +++ b/dockerfiles/tracing/docker-compose.zipkin.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router build: ./router - image: ghcr.io/apollographql/router:v1.54.0 + image: ghcr.io/apollographql/router:v1.55.0-rc.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/zipkin.router.yaml:/etc/config/configuration.yaml diff --git a/helm/chart/router/Chart.yaml b/helm/chart/router/Chart.yaml index 50f1886874..ff312bf296 100644 --- a/helm/chart/router/Chart.yaml +++ b/helm/chart/router/Chart.yaml @@ -20,10 +20,10 @@ type: application # so it matches the shape of our release process and release automation. # By proxy of that decision, this version uses SemVer 2.0.0, though the prefix # of "v" is not included. -version: 1.54.0 +version: 1.55.0-rc.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.54.0" +appVersion: "v1.55.0-rc.0" diff --git a/helm/chart/router/README.md b/helm/chart/router/README.md index 39e6238055..e8821181a6 100644 --- a/helm/chart/router/README.md +++ b/helm/chart/router/README.md @@ -2,7 +2,7 @@ [router](https://github.com/apollographql/router) Rust Graph Routing runtime for Apollo Federation -![Version: 1.54.0](https://img.shields.io/badge/Version-1.54.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.54.0](https://img.shields.io/badge/AppVersion-v1.54.0-informational?style=flat-square) +![Version: 1.55.0-rc.0](https://img.shields.io/badge/Version-1.55.0--rc.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.55.0-rc.0](https://img.shields.io/badge/AppVersion-v1.55.0--rc.0-informational?style=flat-square) ## Prerequisites @@ -11,7 +11,7 @@ ## Get Repo Info ```console -helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.54.0 +helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0-rc.0 ``` ## Install Chart @@ -19,7 +19,7 @@ helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.54.0 **Important:** only helm3 is supported ```console -helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.54.0 --values my-values.yaml +helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0-rc.0 --values my-values.yaml ``` _See [configuration](#configuration) below._ diff --git a/licenses.html b/licenses.html index e50ef97a86..8a7c115dfd 100644 --- a/licenses.html +++ b/licenses.html @@ -44,13 +44,13 @@

Third Party Licenses

Overview of licenses:

    -
  • Apache License 2.0 (432)
  • -
  • MIT License (146)
  • +
  • Apache License 2.0 (448)
  • +
  • MIT License (155)
  • BSD 3-Clause "New" or "Revised" License (11)
  • ISC License (8)
  • +
  • Elastic License 2.0 (6)
  • Mozilla Public License 2.0 (5)
  • BSD 2-Clause "Simplified" License (4)
  • -
  • Elastic License 2.0 (3)
  • Creative Commons Zero v1.0 Universal (2)
  • OpenSSL License (1)
  • Unicode License Agreement - Data Files and Software (2016) (1)
  • @@ -65,6 +65,7 @@

    Used by:

  • aws-config
  • aws-credential-types
  • aws-runtime
  • +
  • aws-sigv4
  • aws-smithy-async
  • aws-smithy-http
  • aws-smithy-json
  • @@ -1716,7 +1717,191 @@

    Used by:

    Apache License 2.0

    Used by:

    +
    +                                 Apache License
    +                           Version 2.0, January 2004
    +                        https://www.apache.org/licenses/
    +
    +   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +   1. Definitions.
    +
    +      "License" shall mean the terms and conditions for use, reproduction,
    +      and distribution as defined by Sections 1 through 9 of this document.
    +
    +      "Licensor" shall mean the copyright owner or entity authorized by
    +      the copyright owner that is granting the License.
    +
    +      "Legal Entity" shall mean the union of the acting entity and all
    +      other entities that control, are controlled by, or are under common
    +      control with that entity. For the purposes of this definition,
    +      "control" means (i) the power, direct or indirect, to cause the
    +      direction or management of such entity, whether by contract or
    +      otherwise, or (ii) ownership of fifty percent (50%) or more of the
    +      outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +      "You" (or "Your") shall mean an individual or Legal Entity
    +      exercising permissions granted by this License.
    +
    +      "Source" form shall mean the preferred form for making modifications,
    +      including but not limited to software source code, documentation
    +      source, and configuration files.
    +
    +      "Object" form shall mean any form resulting from mechanical
    +      transformation or translation of a Source form, including but
    +      not limited to compiled object code, generated documentation,
    +      and conversions to other media types.
    +
    +      "Work" shall mean the work of authorship, whether in Source or
    +      Object form, made available under the License, as indicated by a
    +      copyright notice that is included in or attached to the work
    +      (an example is provided in the Appendix below).
    +
    +      "Derivative Works" shall mean any work, whether in Source or Object
    +      form, that is based on (or derived from) the Work and for which the
    +      editorial revisions, annotations, elaborations, or other modifications
    +      represent, as a whole, an original work of authorship. For the purposes
    +      of this License, Derivative Works shall not include works that remain
    +      separable from, or merely link (or bind by name) to the interfaces of,
    +      the Work and Derivative Works thereof.
    +
    +      "Contribution" shall mean any work of authorship, including
    +      the original version of the Work and any modifications or additions
    +      to that Work or Derivative Works thereof, that is intentionally
    +      submitted to Licensor for inclusion in the Work by the copyright owner
    +      or by an individual or Legal Entity authorized to submit on behalf of
    +      the copyright owner. For the purposes of this definition, "submitted"
    +      means any form of electronic, verbal, or written communication sent
    +      to the Licensor or its representatives, including but not limited to
    +      communication on electronic mailing lists, source code control systems,
    +      and issue tracking systems that are managed by, or on behalf of, the
    +      Licensor for the purpose of discussing and improving the Work, but
    +      excluding communication that is conspicuously marked or otherwise
    +      designated in writing by the copyright owner as "Not a Contribution."
    +
    +      "Contributor" shall mean Licensor and any individual or Legal Entity
    +      on behalf of whom a Contribution has been received by Licensor and
    +      subsequently incorporated within the Work.
    +
    +   2. Grant of Copyright License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      copyright license to reproduce, prepare Derivative Works of,
    +      publicly display, publicly perform, sublicense, and distribute the
    +      Work and such Derivative Works in Source or Object form.
    +
    +   3. Grant of Patent License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      (except as stated in this section) patent license to make, have made,
    +      use, offer to sell, sell, import, and otherwise transfer the Work,
    +      where such license applies only to those patent claims licensable
    +      by such Contributor that are necessarily infringed by their
    +      Contribution(s) alone or by combination of their Contribution(s)
    +      with the Work to which such Contribution(s) was submitted. If You
    +      institute patent litigation against any entity (including a
    +      cross-claim or counterclaim in a lawsuit) alleging that the Work
    +      or a Contribution incorporated within the Work constitutes direct
    +      or contributory patent infringement, then any patent licenses
    +      granted to You under this License for that Work shall terminate
    +      as of the date such litigation is filed.
    +
    +   4. Redistribution. You may reproduce and distribute copies of the
    +      Work or Derivative Works thereof in any medium, with or without
    +      modifications, and in Source or Object form, provided that You
    +      meet the following conditions:
    +
    +      (a) You must give any other recipients of the Work or
    +          Derivative Works a copy of this License; and
    +
    +      (b) You must cause any modified files to carry prominent notices
    +          stating that You changed the files; and
    +
    +      (c) You must retain, in the Source form of any Derivative Works
    +          that You distribute, all copyright, patent, trademark, and
    +          attribution notices from the Source form of the Work,
    +          excluding those notices that do not pertain to any part of
    +          the Derivative Works; and
    +
    +      (d) If the Work includes a "NOTICE" text file as part of its
    +          distribution, then any Derivative Works that You distribute must
    +          include a readable copy of the attribution notices contained
    +          within such NOTICE file, excluding those notices that do not
    +          pertain to any part of the Derivative Works, in at least one
    +          of the following places: within a NOTICE text file distributed
    +          as part of the Derivative Works; within the Source form or
    +          documentation, if provided along with the Derivative Works; or,
    +          within a display generated by the Derivative Works, if and
    +          wherever such third-party notices normally appear. The contents
    +          of the NOTICE file are for informational purposes only and
    +          do not modify the License. You may add Your own attribution
    +          notices within Derivative Works that You distribute, alongside
    +          or as an addendum to the NOTICE text from the Work, provided
    +          that such additional attribution notices cannot be construed
    +          as modifying the License.
    +
    +      You may add Your own copyright statement to Your modifications and
    +      may provide additional or different license terms and conditions
    +      for use, reproduction, or distribution of Your modifications, or
    +      for any such Derivative Works as a whole, provided Your use,
    +      reproduction, and distribution of the Work otherwise complies with
    +      the conditions stated in this License.
    +
    +   5. Submission of Contributions. Unless You explicitly state otherwise,
    +      any Contribution intentionally submitted for inclusion in the Work
    +      by You to the Licensor shall be under the terms and conditions of
    +      this License, without any additional terms or conditions.
    +      Notwithstanding the above, nothing herein shall supersede or modify
    +      the terms of any separate license agreement you may have executed
    +      with Licensor regarding such Contributions.
    +
    +   6. Trademarks. This License does not grant permission to use the trade
    +      names, trademarks, service marks, or product names of the Licensor,
    +      except as required for reasonable and customary use in describing the
    +      origin of the Work and reproducing the content of the NOTICE file.
    +
    +   7. Disclaimer of Warranty. Unless required by applicable law or
    +      agreed to in writing, Licensor provides the Work (and each
    +      Contributor provides its Contributions) on an "AS IS" BASIS,
    +      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +      implied, including, without limitation, any warranties or conditions
    +      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +      PARTICULAR PURPOSE. You are solely responsible for determining the
    +      appropriateness of using or redistributing the Work and assume any
    +      risks associated with Your exercise of permissions under this License.
    +
    +   8. Limitation of Liability. In no event and under no legal theory,
    +      whether in tort (including negligence), contract, or otherwise,
    +      unless required by applicable law (such as deliberate and grossly
    +      negligent acts) or agreed to in writing, shall any Contributor be
    +      liable to You for damages, including any direct, indirect, special,
    +      incidental, or consequential damages of any character arising as a
    +      result of this License or out of the use or inability to use the
    +      Work (including but not limited to damages for loss of goodwill,
    +      work stoppage, computer failure or malfunction, or any and all
    +      other commercial damages or losses), even if such Contributor
    +      has been advised of the possibility of such damages.
    +
    +   9. Accepting Warranty or Additional Liability. While redistributing
    +      the Work or Derivative Works thereof, You may choose to offer,
    +      and charge a fee for, acceptance of support, warranty, indemnity,
    +      or other liability obligations and/or rights consistent with this
    +      License. However, in accepting such obligations, You may act only
    +      on Your own behalf and on Your sole responsibility, not on behalf
    +      of any other Contributor, and only if You agree to indemnify,
    +      defend, and hold each Contributor harmless for any liability
    +      incurred by, or claims asserted against, such Contributor by reason
    +      of your accepting any such warranty or additional liability.
    +
    +   END OF TERMS AND CONDITIONS
    +
    + +
  • +

    Apache License 2.0

    +

    Used by:

    + @@ -2780,6 +2965,7 @@

    Used by:

  • clap_builder
  • clap_derive
  • clap_lex
  • +
  • opentelemetry-proto
                                 Apache License
                            Version 2.0, January 2004
@@ -3620,7 +3806,7 @@ 

Used by:

  • anstyle-parse
  • bytecount
  • diff
  • -
  • normalize-line-endings
  • +
  • predicates
  • winapi
  •                                  Apache License
    @@ -3841,6 +4027,7 @@ 

    Used by:

  • concolor-query
  • crc32fast
  • enum-as-inner
  • +
  • env_filter
  • env_logger
  • graphql-parser
  • hex
  • @@ -5085,6 +5272,7 @@

    Used by:

  • utf-8
  • utf8parse
  • wasm-streams
  • +
  • zerocopy
  •                               Apache License
                             Version 2.0, January 2004
    @@ -8016,6 +8204,7 @@ 

    Used by:

  • bytes-utils
  • cc
  • cfg-if
  • +
  • ci_info
  • cmake
  • concurrent-queue
  • const-random
  • @@ -8030,6 +8219,7 @@

    Used by:

  • derive_arbitrary
  • displaydoc
  • either
  • +
  • envmnt
  • equivalent
  • event-listener
  • fastrand
  • @@ -8040,6 +8230,7 @@

    Used by:

  • fnv
  • form_urlencoded
  • fraction
  • +
  • fsio
  • futures-lite
  • futures-timer
  • gimli
  • @@ -9202,7 +9393,13 @@

    Used by:

    Apache License 2.0

    Used by:

                                  Apache License
                             Version 2.0, January 2004
    @@ -9391,23 +9588,33 @@ 

    Used by:

    file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.
  • Apache License 2.0

    Used by:

                                  Apache License
                             Version 2.0, January 2004
    -                     https://www.apache.org/licenses/
    +                     https://www.apache.org/licenses/LICENSE-2.0
     
     TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
     
    @@ -9612,219 +9819,8 @@ 

    Used by:

    Apache License 2.0

    Used by:

    -
                                  Apache License
    -                        Version 2.0, January 2004
    -                     https://www.apache.org/licenses/LICENSE-2.0
    -
    -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    -
    -1. Definitions.
    -
    -   "License" shall mean the terms and conditions for use, reproduction,
    -   and distribution as defined by Sections 1 through 9 of this document.
    -
    -   "Licensor" shall mean the copyright owner or entity authorized by
    -   the copyright owner that is granting the License.
    -
    -   "Legal Entity" shall mean the union of the acting entity and all
    -   other entities that control, are controlled by, or are under common
    -   control with that entity. For the purposes of this definition,
    -   "control" means (i) the power, direct or indirect, to cause the
    -   direction or management of such entity, whether by contract or
    -   otherwise, or (ii) ownership of fifty percent (50%) or more of the
    -   outstanding shares, or (iii) beneficial ownership of such entity.
    -
    -   "You" (or "Your") shall mean an individual or Legal Entity
    -   exercising permissions granted by this License.
    -
    -   "Source" form shall mean the preferred form for making modifications,
    -   including but not limited to software source code, documentation
    -   source, and configuration files.
    -
    -   "Object" form shall mean any form resulting from mechanical
    -   transformation or translation of a Source form, including but
    -   not limited to compiled object code, generated documentation,
    -   and conversions to other media types.
    -
    -   "Work" shall mean the work of authorship, whether in Source or
    -   Object form, made available under the License, as indicated by a
    -   copyright notice that is included in or attached to the work
    -   (an example is provided in the Appendix below).
    -
    -   "Derivative Works" shall mean any work, whether in Source or Object
    -   form, that is based on (or derived from) the Work and for which the
    -   editorial revisions, annotations, elaborations, or other modifications
    -   represent, as a whole, an original work of authorship. For the purposes
    -   of this License, Derivative Works shall not include works that remain
    -   separable from, or merely link (or bind by name) to the interfaces of,
    -   the Work and Derivative Works thereof.
    -
    -   "Contribution" shall mean any work of authorship, including
    -   the original version of the Work and any modifications or additions
    -   to that Work or Derivative Works thereof, that is intentionally
    -   submitted to Licensor for inclusion in the Work by the copyright owner
    -   or by an individual or Legal Entity authorized to submit on behalf of
    -   the copyright owner. For the purposes of this definition, "submitted"
    -   means any form of electronic, verbal, or written communication sent
    -   to the Licensor or its representatives, including but not limited to
    -   communication on electronic mailing lists, source code control systems,
    -   and issue tracking systems that are managed by, or on behalf of, the
    -   Licensor for the purpose of discussing and improving the Work, but
    -   excluding communication that is conspicuously marked or otherwise
    -   designated in writing by the copyright owner as "Not a Contribution."
    -
    -   "Contributor" shall mean Licensor and any individual or Legal Entity
    -   on behalf of whom a Contribution has been received by Licensor and
    -   subsequently incorporated within the Work.
    -
    -2. Grant of Copyright License. Subject to the terms and conditions of
    -   this License, each Contributor hereby grants to You a perpetual,
    -   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    -   copyright license to reproduce, prepare Derivative Works of,
    -   publicly display, publicly perform, sublicense, and distribute the
    -   Work and such Derivative Works in Source or Object form.
    -
    -3. Grant of Patent License. Subject to the terms and conditions of
    -   this License, each Contributor hereby grants to You a perpetual,
    -   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    -   (except as stated in this section) patent license to make, have made,
    -   use, offer to sell, sell, import, and otherwise transfer the Work,
    -   where such license applies only to those patent claims licensable
    -   by such Contributor that are necessarily infringed by their
    -   Contribution(s) alone or by combination of their Contribution(s)
    -   with the Work to which such Contribution(s) was submitted. If You
    -   institute patent litigation against any entity (including a
    -   cross-claim or counterclaim in a lawsuit) alleging that the Work
    -   or a Contribution incorporated within the Work constitutes direct
    -   or contributory patent infringement, then any patent licenses
    -   granted to You under this License for that Work shall terminate
    -   as of the date such litigation is filed.
    -
    -4. Redistribution. You may reproduce and distribute copies of the
    -   Work or Derivative Works thereof in any medium, with or without
    -   modifications, and in Source or Object form, provided that You
    -   meet the following conditions:
    -
    -   (a) You must give any other recipients of the Work or
    -       Derivative Works a copy of this License; and
    -
    -   (b) You must cause any modified files to carry prominent notices
    -       stating that You changed the files; and
    -
    -   (c) You must retain, in the Source form of any Derivative Works
    -       that You distribute, all copyright, patent, trademark, and
    -       attribution notices from the Source form of the Work,
    -       excluding those notices that do not pertain to any part of
    -       the Derivative Works; and
    -
    -   (d) If the Work includes a "NOTICE" text file as part of its
    -       distribution, then any Derivative Works that You distribute must
    -       include a readable copy of the attribution notices contained
    -       within such NOTICE file, excluding those notices that do not
    -       pertain to any part of the Derivative Works, in at least one
    -       of the following places: within a NOTICE text file distributed
    -       as part of the Derivative Works; within the Source form or
    -       documentation, if provided along with the Derivative Works; or,
    -       within a display generated by the Derivative Works, if and
    -       wherever such third-party notices normally appear. The contents
    -       of the NOTICE file are for informational purposes only and
    -       do not modify the License. You may add Your own attribution
    -       notices within Derivative Works that You distribute, alongside
    -       or as an addendum to the NOTICE text from the Work, provided
    -       that such additional attribution notices cannot be construed
    -       as modifying the License.
    -
    -   You may add Your own copyright statement to Your modifications and
    -   may provide additional or different license terms and conditions
    -   for use, reproduction, or distribution of Your modifications, or
    -   for any such Derivative Works as a whole, provided Your use,
    -   reproduction, and distribution of the Work otherwise complies with
    -   the conditions stated in this License.
    -
    -5. Submission of Contributions. Unless You explicitly state otherwise,
    -   any Contribution intentionally submitted for inclusion in the Work
    -   by You to the Licensor shall be under the terms and conditions of
    -   this License, without any additional terms or conditions.
    -   Notwithstanding the above, nothing herein shall supersede or modify
    -   the terms of any separate license agreement you may have executed
    -   with Licensor regarding such Contributions.
    -
    -6. Trademarks. This License does not grant permission to use the trade
    -   names, trademarks, service marks, or product names of the Licensor,
    -   except as required for reasonable and customary use in describing the
    -   origin of the Work and reproducing the content of the NOTICE file.
    -
    -7. Disclaimer of Warranty. Unless required by applicable law or
    -   agreed to in writing, Licensor provides the Work (and each
    -   Contributor provides its Contributions) on an "AS IS" BASIS,
    -   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    -   implied, including, without limitation, any warranties or conditions
    -   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    -   PARTICULAR PURPOSE. You are solely responsible for determining the
    -   appropriateness of using or redistributing the Work and assume any
    -   risks associated with Your exercise of permissions under this License.
    -
    -8. Limitation of Liability. In no event and under no legal theory,
    -   whether in tort (including negligence), contract, or otherwise,
    -   unless required by applicable law (such as deliberate and grossly
    -   negligent acts) or agreed to in writing, shall any Contributor be
    -   liable to You for damages, including any direct, indirect, special,
    -   incidental, or consequential damages of any character arising as a
    -   result of this License or out of the use or inability to use the
    -   Work (including but not limited to damages for loss of goodwill,
    -   work stoppage, computer failure or malfunction, or any and all
    -   other commercial damages or losses), even if such Contributor
    -   has been advised of the possibility of such damages.
    -
    -9. Accepting Warranty or Additional Liability. While redistributing
    -   the Work or Derivative Works thereof, You may choose to offer,
    -   and charge a fee for, acceptance of support, warranty, indemnity,
    -   or other liability obligations and/or rights consistent with this
    -   License. However, in accepting such obligations, You may act only
    -   on Your own behalf and on Your sole responsibility, not on behalf
    -   of any other Contributor, and only if You agree to indemnify,
    -   defend, and hold each Contributor harmless for any liability
    -   incurred by, or claims asserted against, such Contributor by reason
    -   of your accepting any such warranty or additional liability.
    -
    -END OF TERMS AND CONDITIONS
    -
    -APPENDIX: How to apply the Apache License to your work.
    -
    -   To apply the Apache License to your work, attach the following
    -   boilerplate notice, with the fields enclosed by brackets "[]"
    -   replaced with your own identifying information. (Don't include
    -   the brackets!)  The text should be enclosed in the appropriate
    -   comment syntax for the file format. We also recommend that a
    -   file or class name and description of purpose be included on the
    -   same "printed page" as the copyright notice for easier
    -   identification within third-party archives.
    -
    -Copyright [yyyy] [name of copyright owner]
    -
    -Licensed under the Apache License, Version 2.0 (the "License");
    -you may not use this file except in compliance with the License.
    -You may obtain a copy of the License at
    -
    -	https://www.apache.org/licenses/LICENSE-2.0
    -
    -Unless required by applicable law or agreed to in writing, software
    -distributed under the License is distributed on an "AS IS" BASIS,
    -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    -See the License for the specific language governing permissions and
    -limitations under the License.
    -
    -
  • -
  • -

    Apache License 2.0

    -

    Used by:

    -
                                  Apache License
                             Version 2.0, January 2004
    @@ -10840,6 +10836,43 @@ 

    Used by:

    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
    # Contributing
    +
    +## License
    +
    +Licensed under either of
    +
    + * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
    + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
    +
    +at your option.
    +
    +### Contribution
    +
    +Unless you explicitly state otherwise, any contribution intentionally submitted
    +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
    +additional terms or conditions.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
    // Licensed under the Apache License, Version 2.0
    +// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
    +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
    +// All files in the project carrying such notice may not be copied, modified, or distributed
    +// except according to those terms.
     
  • @@ -11483,7 +11516,6 @@

    Used by:

  • async-graphql-derive
  • async-graphql-parser
  • async-graphql-value
  • -
  • buildstructor
  • deno-proc-macro-rules
  • deno-proc-macro-rules-macros
  • dunce
  • @@ -11498,8 +11530,8 @@

    Used by:

  • md5
  • num-cmp
  • prost
  • +
  • rand_core
  • rhai_codegen
  • -
  • serde_derive_default
  • siphasher
  • system-configuration
  • system-configuration-sys
  • @@ -11587,6 +11619,92 @@

    Used by:

    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +
    + +
  • +

    Apache License 2.0

    +

    Used by:

    + +
    Copyright [2022] [Bryn Cooke]
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
    Copyright [2023] [Bryn Cooke]
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
    Licensed under the Apache License, Version 2.0
    +<LICENSE-APACHE or
    +http://www.apache.org/licenses/LICENSE-2.0> or the MIT
    +license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
    +at your option. All files in the project carrying such
    +notice may not be copied, modified, or distributed except
    +according to those terms.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
    MIT OR Apache-2.0
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
    MIT OR Apache-2.0
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
    MIT or Apache-2.0
     
  • @@ -12149,6 +12267,9 @@

    Elastic License 2.0

    Used by:

    Copyright 2021 Apollo Graph, Inc.
     
    @@ -12413,36 +12534,6 @@ 

    Used by:

    // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -
    -
  • -
  • -

    ISC License

    -

    Used by:

    - -
    // Copyright 2021 Brian Smith.
    -//
    -// Permission to use, copy, modify, and/or distribute this software for any
    -// purpose with or without fee is hereby granted, provided that the above
    -// copyright notice and this permission notice appear in all copies.
    -//
    -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
    -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
    -// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    -// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    -// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    -// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
    -
    -#[test]
    -fn cert_without_extensions_test() {
    -    // Check the certificate is valid with
    -    // `openssl x509 -in cert_without_extensions.der -inform DER -text -noout`
    -    const CERT_WITHOUT_EXTENSIONS_DER: &[u8] = include_bytes!("cert_without_extensions.der");
    -
    -    assert!(webpki::EndEntityCert::try_from(CERT_WITHOUT_EXTENSIONS_DER).is_ok());
    -}
     
  • @@ -12512,6 +12603,7 @@

    ISC License

    Used by:

    ISC License:
     
    @@ -13233,6 +13325,66 @@ 

    Used by:

    shall be included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2019 Carl Lerche
    +
    +Permission is hereby granted, free of charge, to any
    +person obtaining a copy of this software and associated
    +documentation files (the "Software"), to deal in the
    +Software without restriction, including without
    +limitation the rights to use, copy, modify, merge,
    +publish, distribute, sublicense, and/or sell copies of
    +the Software, and to permit persons to whom the Software
    +is furnished to do so, subject to the following
    +conditions:
    +
    +The above copyright notice and this permission notice
    +shall be included in all copies or substantial portions
    +of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
    +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
    +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
    +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
    +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    +DEALINGS IN THE SOFTWARE.
    +
    +Copyright (c) 2018 David Tolnay
    +
    +Permission is hereby granted, free of charge, to any
    +person obtaining a copy of this software and associated
    +documentation files (the "Software"), to deal in the
    +Software without restriction, including without
    +limitation the rights to use, copy, modify, merge,
    +publish, distribute, sublicense, and/or sell copies of
    +the Software, and to permit persons to whom the Software
    +is furnished to do so, subject to the following
    +conditions:
    +
    +The above copyright notice and this permission notice
    +shall be included in all copies or substantial portions
    +of the Software.
    +
     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
     ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
     TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    @@ -14239,8 +14391,6 @@ 

    Used by:

    MIT License

    Used by:

      -
    • async-stream
    • -
    • async-stream-impl
    • base64-simd
    • convert_case
    • cookie-factory
    • @@ -14251,7 +14401,6 @@

      Used by:

    • deno_url
    • deno_web
    • deno_webidl
    • -
    • difflib
    • jsonschema
    • lazy-regex-proc_macros
    • number_prefix
    • @@ -14553,6 +14702,37 @@

      Used by:

      MIT License

      Used by:

      +
      The MIT License (MIT)
      +
      +Copyright (c) 2014 Mathijs van de Nes
      +
      +Permission is hereby granted, free of charge, to any person obtaining a copy
      +of this software and associated documentation files (the "Software"), to deal
      +in the Software without restriction, including without limitation the rights
      +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      +copies of the Software, and to permit persons to whom the Software is
      +furnished to do so, subject to the following conditions:
      +
      +The above copyright notice and this permission notice shall be included in all
      +copies or substantial portions of the Software.
      +
      +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
      +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
      +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
      +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
      +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
      +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
      +SOFTWARE.
      +
      + +
    • +

      MIT License

      +

      Used by:

      + @@ -14589,7 +14769,6 @@

      Used by:

    • globset
    • memchr
    • regex-automata
    • -
    • termcolor
    • walkdir
    The MIT License (MIT)
    @@ -15078,6 +15257,24 @@ 

    Used by:

    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    This project is dual-licensed under the Unlicense and MIT licenses.
    +
    +You may use this code under the terms of either license.
     
  • @@ -15475,7 +15672,6 @@

    Used by:

    Mozilla Public License 2.0

    Used by:

    Mozilla Public License Version 2.0
    @@ -15858,8 +16054,8 @@ 

    Used by:

    Mozilla Public License 2.0

    Used by:

    Mozilla Public License Version 2.0
     ==================================
    @@ -16234,6 +16430,35 @@ 

    Used by:

    This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. +
    +
  • +
  • +

    Mozilla Public License 2.0

    +

    Used by:

    + +
    This packge contains a modified version of ca-bundle.crt:
    +
    +ca-bundle.crt -- Bundle of CA Root Certificates
    +
    +Certificate data from Mozilla as of: Thu Nov  3 19:04:19 2011#
    +This is a bundle of X.509 certificates of public Certificate Authorities
    +(CA). These were automatically extracted from Mozilla's root certificates
    +file (certdata.txt).  This file can be found in the mozilla source tree:
    +http://mxr.mozilla.org/mozilla/source/security/nss/lib/ckfw/builtins/certdata.txt?raw=1#
    +It contains the certificates in PEM format and therefore
    +can be directly used with curl / libcurl / php_curl, or with
    +an Apache+mod_ssl webserver for SSL client authentication.
    +Just configure this file as the SSLCACertificateFile.#
    +
    +***** BEGIN LICENSE BLOCK *****
    +This Source Code Form is subject to the terms of the Mozilla Public License,
    +v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain
    +one at http://mozilla.org/MPL/2.0/.
    +
    +***** END LICENSE BLOCK *****
    +@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $
     
  • @@ -16304,50 +16529,26 @@

    Used by:

    UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE
     
    -See Terms of Use <https://www.unicode.org/copyright.html>
    -for definitions of Unicode Inc.’s Data Files and Software.
    +Unicode Data Files include all data files under the directories http://www.unicode.org/Public/, http://www.unicode.org/reports/, http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and http://www.unicode.org/utility/trac/browser/.
     
    -NOTICE TO USER: Carefully read the following legal agreement.
    -BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S
    -DATA FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"),
    -YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
    -TERMS AND CONDITIONS OF THIS AGREEMENT.
    -IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE
    -THE DATA FILES OR SOFTWARE.
    +Unicode Data Files do not include PDF online code charts under the directory http://www.unicode.org/Public/.
    +
    +Software includes any source code published in the Unicode Standard or under the directories http://www.unicode.org/Public/, http://www.unicode.org/reports/, http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and http://www.unicode.org/utility/trac/browser/.
    +
    +NOTICE TO USER: Carefully read the following legal agreement. BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S DATA FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
     
     COPYRIGHT AND PERMISSION NOTICE
     
    -Copyright © 1991-2022 Unicode, Inc. All rights reserved.
    -Distributed under the Terms of Use in https://www.unicode.org/copyright.html.
    +Copyright © 1991-2016 Unicode, Inc. All rights reserved. Distributed under the Terms of Use in http://www.unicode.org/copyright.html.
     
    -Permission is hereby granted, free of charge, to any person obtaining
    -a copy of the Unicode data files and any associated documentation
    -(the "Data Files") or Unicode software and any associated documentation
    -(the "Software") to deal in the Data Files or Software
    -without restriction, including without limitation the rights to use,
    -copy, modify, merge, publish, distribute, and/or sell copies of
    -the Data Files or Software, and to permit persons to whom the Data Files
    -or Software are furnished to do so, provided that either
    -(a) this copyright and permission notice appear with all copies
    -of the Data Files or Software, or
    -(b) this copyright and permission notice appear in associated
    -Documentation.
    -
    -THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF
    -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
    -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    -NONINFRINGEMENT OF THIRD PARTY RIGHTS.
    -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS
    -NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL
    -DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
    -DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
    -TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
    -PERFORMANCE OF THE DATA FILES OR SOFTWARE.
    -
    -Except as contained in this notice, the name of a copyright holder
    -shall not be used in advertising or otherwise to promote the sale,
    -use or other dealings in these Data Files or Software without prior
    -written authorization of the copyright holder.
    +Permission is hereby granted, free of charge, to any person obtaining a copy of the Unicode data files and any associated documentation (the "Data Files") or Unicode software and any associated documentation (the "Software") to deal in the Data Files or Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the Data Files or Software, and to permit persons to whom the Data Files or Software are furnished to do so, provided that either
    +
    +     (a) this copyright and permission notice appear with all copies of the Data Files or Software, or
    +     (b) this copyright and permission notice appear in associated Documentation.
    +
    +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA FILES OR SOFTWARE.
    +
    +Except as contained in this notice, the name of a copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in these Data Files or Software without prior written authorization of the copyright holder.
     
  • diff --git a/scripts/install.sh b/scripts/install.sh index 0447b4c144..a3637725a9 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -11,7 +11,7 @@ BINARY_DOWNLOAD_PREFIX="https://github.com/apollographql/router/releases/downloa # Router version defined in apollo-router's Cargo.toml # Note: Change this line manually during the release steps. -PACKAGE_VERSION="v1.54.0" +PACKAGE_VERSION="v1.55.0-rc.0" download_binary() { downloader --check From 05057106240d444093dad4dc0d5001acbe84a14c Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 18 Sep 2024 14:30:15 +0200 Subject: [PATCH 31/56] remove test-span (#6016) --- Cargo.lock | 46 ---------------------------------------- apollo-router/Cargo.toml | 1 - 2 files changed, 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cefb7bcb6a..280f936fd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -364,7 +364,6 @@ dependencies = [ "sys-info", "tempfile", "test-log", - "test-span", "thiserror", "tikv-jemallocator", "time", @@ -1989,16 +1988,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "daggy" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91a9304e55e9d601a39ae4deaba85406d5c0980e106f65afcf0460e9af1e7602" -dependencies = [ - "petgraph", - "serde", -] - [[package]] name = "darling" version = "0.20.10" @@ -3979,9 +3968,6 @@ name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" -dependencies = [ - "serde", -] [[package]] name = "linkme" @@ -6523,38 +6509,6 @@ dependencies = [ "syn 2.0.76", ] -[[package]] -name = "test-span" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ada8d8deb7460522e606aa5a66568a7ef9f61dfeee8a3c563c69f4d43b1c96" -dependencies = [ - "daggy", - "derivative", - "indexmap 2.2.6", - "linked-hash-map", - "once_cell", - "serde", - "serde_json", - "test-span-macro", - "tokio", - "tracing", - "tracing-core", - "tracing-futures", - "tracing-subscriber", -] - -[[package]] -name = "test-span-macro" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a30d7cc64c67cb4ac13f4eeb08252f1ae9f1558cd30a336380da6a0a9cf0aef" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.76", -] - [[package]] name = "text-size" version = "1.1.1" diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 00c0da7a66..f4daceed74 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -336,7 +336,6 @@ tempfile.workspace = true test-log = { version = "0.2.16", default-features = false, features = [ "trace", ] } -test-span = "0.8.0" basic-toml = "0.1.9" tower-test = "0.4.0" From 73757d5e1e6bd8656015c1b7b32f1f12aee8e3d8 Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:24:43 -0500 Subject: [PATCH 32/56] chore(federation): regenerate snapshots with fed 2.7 (#6024) --- .../tests/query_plan/build_query_plan_support.rs | 5 ++--- ...d_back_sibling_typename_to_interface_object.graphql | 10 +++++++--- .../supergraphs/adjacent_mutations_get_merged.graphql | 10 +++++++--- .../supergraphs/allows_setting_down_to_1.graphql | 10 +++++++--- ...her_mix_of_fragments_indirection_and_unions.graphql | 10 +++++++--- ...from_the_root_when_a_more_direct_one_exists.graphql | 10 +++++++--- .../supergraphs/avoids_unnecessary_fetches.graphql | 10 +++++++--- .../supergraphs/basic_subscription_query_plan.graphql | 10 +++++++--- .../basic_subscription_with_single_subgraph.graphql | 10 +++++++--- .../can_be_set_to_an_arbitrary_number.graphql | 10 +++++++--- ...e_they_only_partially_apply_in_entity_fetch.graphql | 10 +++++++--- ...ere_they_only_partially_apply_in_root_fetch.graphql | 10 +++++++--- ...erface_object_from_an_interface_object_type.graphql | 10 +++++++--- .../can_use_a_key_on_an_interface_object_type.graphql | 10 +++++++--- ...ect_type_even_for_a_concrete_implementation.graphql | 10 +++++++--- ...eration_from_multiple_subgraphs_in_parallel.graphql | 10 +++++++--- ...ome_non_individually_optimal_branch_options.graphql | 10 +++++++--- ...e_where_there_is_too_many_plans_to_consider.graphql | 10 +++++++--- .../supergraphs/defer_gets_stripped_out.graphql | 10 +++++++--- ...or_on_some_complex_fetch_group_dependencies.graphql | 10 +++++++--- ...gments_when_interface_subtyping_is_involved.graphql | 10 +++++++--- ...ing_on_a_key_field_to_fetch_that_same_field.graphql | 10 +++++++--- ...n_an_interface_object_directly_for_typename.graphql | 10 +++++++--- ...y_if_a_specific_implementation_is_requested.graphql | 10 +++++++--- .../ensures_sanitization_applies_repeatedly.graphql | 10 +++++++--- .../field_covariance_and_type_explosion.graphql | 10 +++++++--- ...secting_parent_type_and_directive_condition.graphql | 10 +++++++--- ...cal_generate_their_own_fragment_definitions.graphql | 10 +++++++--- .../handle_subgraph_with_hypen_in_the_name.graphql | 10 +++++++--- .../handle_very_non_graph_subgraph_name.graphql | 10 +++++++--- ...les_case_of_key_chains_in_parallel_requires.graphql | 10 +++++++--- ...es_fragments_with_interface_field_subtyping.graphql | 10 +++++++--- ...les_mix_of_fragments_indirection_and_unions.graphql | 10 +++++++--- ...ndles_multiple_conditions_on_abstract_types.graphql | 10 +++++++--- ...ple_requires_involving_different_nestedness.graphql | 10 +++++++--- ...andles_non_intersecting_fragment_conditions.graphql | 10 +++++++--- ..._matching_value_types_under_interface_field.graphql | 10 +++++++--- ...ion_when_query_starts_with_interface_object.graphql | 10 +++++++--- .../handles_requires_from_supergraph.graphql | 10 +++++++--- ..._root_operation_shareable_in_many_subgraphs.graphql | 10 +++++++--- .../supergraphs/handles_simple_requires.graphql | 10 +++++++--- .../handles_spread_unions_correctly.graphql | 10 +++++++--- ...th_no_common_supertype_at_the_same_merge_at.graphql | 10 +++++++--- .../interface_interface_interaction.graphql | 10 +++++++--- ...ace_interaction_but_no_need_to_type_explode.graphql | 10 +++++++--- .../supergraphs/interface_union_interaction.graphql | 10 +++++++--- ...ion_interaction_but_no_need_to_type_explode.graphql | 10 +++++++--- ...llow_providing_fields_for_only_some_subtype.graphql | 10 +++++++--- .../it_avoid_fragments_usable_only_once.graphql | 10 +++++++--- ...lts_that_may_have_to_be_filtered_with_lists.graphql | 10 +++++++--- .../it_can_require_at_inaccessible_fields.graphql | 10 +++++++--- ...ot_needlessly_consider_options_for_typename.graphql | 10 +++++++--- ...agments_that_are_not_valid_for_the_subgraph.graphql | 10 +++++++--- ...it_executes_mutation_operations_in_sequence.graphql | 10 +++++++--- .../it_handes_diamond_shape_depedencies.graphql | 10 +++++++--- ..._at_requires_triggered_within_a_conditional.graphql | 10 +++++++--- ...dles_an_at_requires_triggered_conditionally.graphql | 10 +++++++--- ...res_where_multiple_conditional_are_involved.graphql | 10 +++++++--- .../it_handles_complex_require_chain.graphql | 10 +++++++--- ...graph_where_some_subtyping_relation_differs.graphql | 10 +++++++--- ...here_some_union_membership_relation_differs.graphql | 10 +++++++--- ...t_handles_fragments_with_one_non_leaf_field.graphql | 10 +++++++--- ...t_handles_interface_object_in_nested_entity.graphql | 10 +++++++--- ...nput_rewrites_when_cloning_dependency_graph.graphql | 10 +++++++--- .../it_handles_longer_require_chain.graphql | 10 +++++++--- ...les_multiple_requires_with_multiple_fetches.graphql | 10 +++++++--- ...tiple_requires_within_the_same_entity_fetch.graphql | 10 +++++++--- .../it_handles_nested_fragment_generation.graphql | 10 +++++++--- ..._require_chain_not_ending_in_original_group.graphql | 10 +++++++--- ..._type_of_field_provided_by_interface_object.graphql | 10 +++++++--- .../it_handles_simple_require_chain.graphql | 10 +++++++--- ...s_equivalent_fragments_that_arent_identical.graphql | 10 +++++++--- .../supergraphs/it_migrates_skip_include.graphql | 10 +++++++--- .../supergraphs/it_preservers_aliased_typename.graphql | 10 +++++++--- ...reserves_directives_when_fragment_is_reused.graphql | 10 +++++++--- ...preserves_directives_when_fragment_not_used.graphql | 10 +++++++--- ..._outer_one_has_directives_and_is_eliminated.graphql | 10 +++++++--- ...eld_when_one_is_also_a_key_to_reach_another.graphql | 10 +++++++--- ...it_respects_generate_query_fragments_option.graphql | 10 +++++++--- .../supergraphs/it_works_on_interfaces.graphql | 10 +++++++--- .../query_plan/supergraphs/it_works_on_unions.graphql | 10 +++++++--- .../it_works_with_nested_fragments_1.graphql | 10 +++++++--- ...hen_only_the_nested_fragment_gets_preserved.graphql | 10 +++++++--- .../supergraphs/it_works_with_nested_provides.graphql | 10 +++++++--- ...for_types_only_reachable_by_the_at_provides.graphql | 10 +++++++--- ...s_not_at_top_level_of_selection_of_requires.graphql | 10 +++++++--- ...tives_multiple_applications_differing_order.graphql | 10 +++++++--- ...es_multiple_applications_differing_quantity.graphql | 10 +++++++--- ..._directives_multiple_applications_identical.graphql | 10 +++++++--- ...g_skip_and_include_directives_with_fragment.graphql | 10 +++++++--- ...kip_and_include_directives_without_fragment.graphql | 10 +++++++--- ...cation_overflow_in_reduce_options_if_needed.graphql | 10 +++++++--- ...rseting_parent_type_and_directive_condition.graphql | 10 +++++++--- .../non_adjacent_mutations_do_not_get_merged.graphql | 10 +++++++--- .../only_uses_an_interface_object_if_it_can.graphql | 10 +++++++--- .../pick_keys_that_minimize_fetches.graphql | 10 +++++++--- ...for_inline_fragments_without_type_condition.graphql | 10 +++++++--- ..._planner_option_reuse_query_fragments_false.graphql | 10 +++++++--- ...y_planner_option_reuse_query_fragments_true.graphql | 10 +++++++--- ...e_not_overwritten_after_removing_directives.graphql | 10 +++++++--- .../skip_type_explosion_early_if_unnecessary.graphql | 10 +++++++--- ...used_in_the_directives_applied_to_the_query.graphql | 10 +++++++--- ...n_level_are_passed_down_to_subgraph_queries.graphql | 10 +++++++--- ...tations_are_passed_down_to_subgraph_queries.graphql | 10 +++++++--- ...es_with_arguments_applied_on_queries_are_ok.graphql | 10 +++++++--- ..._not_create_cycle_in_fetch_dependency_graph.graphql | 10 +++++++--- .../test_merging_fetches_reset_cached_costs.graphql | 10 +++++++--- ...efer_with_a_subcription_results_in_an_error.graphql | 10 +++++++--- .../supergraphs/union_interface_interaction.graphql | 10 +++++++--- ...ace_interaction_but_no_need_to_type_explode.graphql | 10 +++++++--- .../supergraphs/union_union_interaction.graphql | 10 +++++++--- ...ion_interaction_but_no_need_to_type_explode.graphql | 10 +++++++--- ...ecessary_include_is_stripped_from_fragments.graphql | 10 +++++++--- .../query_plan/supergraphs/works_when_unset.graphql | 10 +++++++--- .../supergraphs/works_with_key_chains.graphql | 10 +++++++--- 115 files changed, 800 insertions(+), 345 deletions(-) diff --git a/apollo-federation/tests/query_plan/build_query_plan_support.rs b/apollo-federation/tests/query_plan/build_query_plan_support.rs index ed70798f2f..f73c3117f7 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_support.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_support.rs @@ -14,8 +14,7 @@ use sha1::Digest; const ROVER_FEDERATION_VERSION: &str = "2.7.4"; -// TODO: use 2.7 when join v0.4 is fully supported in this crate -const IMPLICIT_LINK_DIRECTIVE: &str = r#"@link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])"#; +const DEFAULT_LINK_DIRECTIVE: &str = r#"@link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])"#; /// Runs composition on the given subgraph schemas and return `(api_schema, query_planner)` /// @@ -110,7 +109,7 @@ pub(crate) fn compose( .map(|(name, schema)| { ( *name, - format!("extend schema {IMPLICIT_LINK_DIRECTIVE}\n\n{}", schema,), + format!("extend schema {DEFAULT_LINK_DIRECTIVE}\n\n{}", schema,), ) }) .collect(); diff --git a/apollo-federation/tests/query_plan/supergraphs/add_back_sibling_typename_to_interface_object.graphql b/apollo-federation/tests/query_plan/supergraphs/add_back_sibling_typename_to_interface_object.graphql index 5263932418..08d3c44ea2 100644 --- a/apollo-federation/tests/query_plan/supergraphs/add_back_sibling_typename_to_interface_object.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/add_back_sibling_typename_to_interface_object.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: f041374db74b06004feb7864eece06a163328b37 +# Composed from subgraphs with hash: 9c69e32dbbd50fb46ea218c3581b08f4f084b9b9 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -44,6 +46,8 @@ interface Item name: String! @join__field(graph: SUBGRAPH1) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/adjacent_mutations_get_merged.graphql b/apollo-federation/tests/query_plan/supergraphs/adjacent_mutations_get_merged.graphql index e99930397b..ae84cc8ed8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/adjacent_mutations_get_merged.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/adjacent_mutations_get_merged.graphql @@ -1,15 +1,17 @@ -# Composed from subgraphs with hash: 2655e7da6754e73955fece01d7cbb5f21085bdbb +# Composed from subgraphs with hash: 54adb76945715a2ce0e068d2635d71ca4166b289 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query mutation: Mutation } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -30,6 +32,8 @@ type Foo baz: Int @join__field(graph: SUBGRAPHB) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/allows_setting_down_to_1.graphql b/apollo-federation/tests/query_plan/supergraphs/allows_setting_down_to_1.graphql index 0b6baf2e68..42f3b4b125 100644 --- a/apollo-federation/tests/query_plan/supergraphs/allows_setting_down_to_1.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/allows_setting_down_to_1.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: bf831e2e6890f60e5c8e93bc52ce549323cb23e8 +# Composed from subgraphs with hash: 91d4d0661d413b60ae2ed463c074c4180d7b849e schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/another_mix_of_fragments_indirection_and_unions.graphql b/apollo-federation/tests/query_plan/supergraphs/another_mix_of_fragments_indirection_and_unions.graphql index cafda1ceee..9b6bb51265 100644 --- a/apollo-federation/tests/query_plan/supergraphs/another_mix_of_fragments_indirection_and_unions.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/another_mix_of_fragments_indirection_and_unions.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 025701346c4869b221f7700ee29514f70ca1bb6c +# Composed from subgraphs with hash: b0bcf31c6c8c64e53e4286dbf397738e41ecbda7 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ interface I id2: ID! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/avoid_considering_indirect_paths_from_the_root_when_a_more_direct_one_exists.graphql b/apollo-federation/tests/query_plan/supergraphs/avoid_considering_indirect_paths_from_the_root_when_a_more_direct_one_exists.graphql index 2a7e9c07f9..b5d2335e2c 100644 --- a/apollo-federation/tests/query_plan/supergraphs/avoid_considering_indirect_paths_from_the_root_when_a_more_direct_one_exists.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/avoid_considering_indirect_paths_from_the_root_when_a_more_direct_one_exists.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 995342f0aeb7c35ebe233102083b817ae5d9b0a8 +# Composed from subgraphs with hash: 9df3e1f539626e2d33ad1aeab8fa51e27fd1f441 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/avoids_unnecessary_fetches.graphql b/apollo-federation/tests/query_plan/supergraphs/avoids_unnecessary_fetches.graphql index ebd8d020f0..7ea5c3595d 100644 --- a/apollo-federation/tests/query_plan/supergraphs/avoids_unnecessary_fetches.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/avoids_unnecessary_fetches.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 1fd25bcad80101354eb093f4847039ee2ef80241 +# Composed from subgraphs with hash: 8ba19e06c01ab40b92eda93ac77de08c57856299 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -30,6 +32,8 @@ type A idA1: ID! @join__field(graph: SUBGRAPH3) @join__field(graph: SUBGRAPH4) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/basic_subscription_query_plan.graphql b/apollo-federation/tests/query_plan/supergraphs/basic_subscription_query_plan.graphql index 3af33311f3..8b559e6727 100644 --- a/apollo-federation/tests/query_plan/supergraphs/basic_subscription_query_plan.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/basic_subscription_query_plan.graphql @@ -1,15 +1,17 @@ -# Composed from subgraphs with hash: 8a2b0d4121c001b0263da2dd5f1a7f77801c3a80 +# Composed from subgraphs with hash: f7613316e8b925d2e1fb7f78b351920adfa4a595 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query subscription: Subscription } +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) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -21,6 +23,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/basic_subscription_with_single_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/basic_subscription_with_single_subgraph.graphql index 4bcaf7ed8f..e917252972 100644 --- a/apollo-federation/tests/query_plan/supergraphs/basic_subscription_with_single_subgraph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/basic_subscription_with_single_subgraph.graphql @@ -1,15 +1,17 @@ -# Composed from subgraphs with hash: e6e4f1ffd5eae3125cc831f8eb1e46200243da74 +# Composed from subgraphs with hash: 409fcb3d17fb926642cf9483b5c1db292c219eb1 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query subscription: Subscription } +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) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -21,6 +23,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/can_be_set_to_an_arbitrary_number.graphql b/apollo-federation/tests/query_plan/supergraphs/can_be_set_to_an_arbitrary_number.graphql index 0b6baf2e68..42f3b4b125 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_be_set_to_an_arbitrary_number.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_be_set_to_an_arbitrary_number.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: bf831e2e6890f60e5c8e93bc52ce549323cb23e8 +# Composed from subgraphs with hash: 91d4d0661d413b60ae2ed463c074c4180d7b849e schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_entity_fetch.graphql b/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_entity_fetch.graphql index f470a875f3..077541aced 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_entity_fetch.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_entity_fetch.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 69f84bc4036179c07a518c8c9b871bd0850f1606 +# Composed from subgraphs with hash: 24f37faeded16eb10b0ffc6c3c2a0e936f406801 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_root_fetch.graphql b/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_root_fetch.graphql index 55a80e69c6..4d21c8ec70 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_root_fetch.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_root_fetch.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: fdeaf767c1e72134946220497d72c7db3cebbe19 +# Composed from subgraphs with hash: d4c3168300df54a934d76deb9884e98ba647a676 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_from_an_interface_object_type.graphql b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_from_an_interface_object_type.graphql index 14f041319c..97189f9365 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_from_an_interface_object_type.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_from_an_interface_object_type.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 43bf934e8826f0b4d17867095ea2025fb352287a +# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I y: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type.graphql b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type.graphql index 14f041319c..97189f9365 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 43bf934e8826f0b4d17867095ea2025fb352287a +# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I y: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation.graphql b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation.graphql index 14f041319c..97189f9365 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 43bf934e8826f0b4d17867095ea2025fb352287a +# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I y: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/can_use_same_root_operation_from_multiple_subgraphs_in_parallel.graphql b/apollo-federation/tests/query_plan/supergraphs/can_use_same_root_operation_from_multiple_subgraphs_in_parallel.graphql index 66e59ae104..843563aa3f 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_use_same_root_operation_from_multiple_subgraphs_in_parallel.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_use_same_root_operation_from_multiple_subgraphs_in_parallel.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: ef972ffb2fc3cdf19f816cdc10e29756fb3f0656 +# Composed from subgraphs with hash: 34751a1a79473dcec3aad139f107e5b73a855e0d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/correctly_generate_plan_built_from_some_non_individually_optimal_branch_options.graphql b/apollo-federation/tests/query_plan/supergraphs/correctly_generate_plan_built_from_some_non_individually_optimal_branch_options.graphql index 2766e3b307..56e9fef51e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/correctly_generate_plan_built_from_some_non_individually_optimal_branch_options.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/correctly_generate_plan_built_from_some_non_individually_optimal_branch_options.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 38b15e780cba3d9d7cb6288e027386e3d612102a +# Composed from subgraphs with hash: 6ca9a3e7d089605b227c291630c3f089dde1f38d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql b/apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql index cfa0ce48ca..80f221ab6f 100644 --- a/apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 569b27fb405f035347590f869510e5cc78058214 +# Composed from subgraphs with hash: 8a8e74cff42f95e71955aa96d62ef9f9589739d5 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_gets_stripped_out.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_gets_stripped_out.graphql index cda9ad539f..ec6c89f59b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_gets_stripped_out.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_gets_stripped_out.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 468d7f03b0a1c21793bcfc5e41d6ddeea4e94ed1 +# Composed from subgraphs with hash: 3e20191edc9bb3ef7d687ce811c347718e6e4100 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/does_not_error_on_some_complex_fetch_group_dependencies.graphql b/apollo-federation/tests/query_plan/supergraphs/does_not_error_on_some_complex_fetch_group_dependencies.graphql index dbc5271859..950263d878 100644 --- a/apollo-federation/tests/query_plan/supergraphs/does_not_error_on_some_complex_fetch_group_dependencies.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/does_not_error_on_some_complex_fetch_group_dependencies.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: f4f751d2b348c0947b2f1dbca4cea1c987ff7d02 +# Composed from subgraphs with hash: 604dead6fd018b16380a1abf51ba199acbdb82f2 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/does_not_error_out_handling_fragments_when_interface_subtyping_is_involved.graphql b/apollo-federation/tests/query_plan/supergraphs/does_not_error_out_handling_fragments_when_interface_subtyping_is_involved.graphql index 585bd1fea2..5137fdf735 100644 --- a/apollo-federation/tests/query_plan/supergraphs/does_not_error_out_handling_fragments_when_interface_subtyping_is_involved.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/does_not_error_out_handling_fragments_when_interface_subtyping_is_involved.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: b62548892fe6cba44778c9e402abb8ddf8edbac9 +# Composed from subgraphs with hash: 85a2b796ad6f1553ddbc0bf5f4722e602f475090 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -47,6 +49,8 @@ interface IB v1: Int! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/does_not_evaluate_plans_relying_on_a_key_field_to_fetch_that_same_field.graphql b/apollo-federation/tests/query_plan/supergraphs/does_not_evaluate_plans_relying_on_a_key_field_to_fetch_that_same_field.graphql index a55159cae8..674a08c9f2 100644 --- a/apollo-federation/tests/query_plan/supergraphs/does_not_evaluate_plans_relying_on_a_key_field_to_fetch_that_same_field.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/does_not_evaluate_plans_relying_on_a_key_field_to_fetch_that_same_field.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 15c059c34b90d54a9c27d2ad67c89307a1280a1f +# Composed from subgraphs with hash: e4cc087ad60a261439df7beadccbe9b4a31eaa8d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_for_typename.graphql b/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_for_typename.graphql index 14f041319c..97189f9365 100644 --- a/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_for_typename.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_for_typename.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 43bf934e8826f0b4d17867095ea2025fb352287a +# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I y: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is_requested.graphql b/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is_requested.graphql index 14f041319c..97189f9365 100644 --- a/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is_requested.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is_requested.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 43bf934e8826f0b4d17867095ea2025fb352287a +# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I y: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql b/apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql index ab27753e5d..f62dcb21b5 100644 --- a/apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 39d8b2af4d4c017bda2b76a66d2128757ae8ac5d +# Composed from subgraphs with hash: 5a37dc86c56cab4caab8bb5d9606eb4c7df0d804 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql b/apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql index c107e04990..923bde8335 100644 --- a/apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: cea83739e9f98b70a1185463f8b9c3c17f2064c8 +# Composed from subgraphs with hash: 2254345d779de0a2850b6c7a5db722f1989a774c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -26,6 +28,8 @@ interface Interface field: Interface } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/fragment_with_intersecting_parent_type_and_directive_condition.graphql b/apollo-federation/tests/query_plan/supergraphs/fragment_with_intersecting_parent_type_and_directive_condition.graphql index 4417e0ea0b..1e2a373495 100644 --- a/apollo-federation/tests/query_plan/supergraphs/fragment_with_intersecting_parent_type_and_directive_condition.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/fragment_with_intersecting_parent_type_and_directive_condition.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 059bed56ce74dbb40643a1561b79514872f0ab60 +# Composed from subgraphs with hash: e2393609250b71261acc4089006bc8a14627d488 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -35,6 +37,8 @@ interface I2 title: String } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/fragments_that_share_a_hash_but_are_not_identical_generate_their_own_fragment_definitions.graphql b/apollo-federation/tests/query_plan/supergraphs/fragments_that_share_a_hash_but_are_not_identical_generate_their_own_fragment_definitions.graphql index eaf9c3e94d..a745ed6263 100644 --- a/apollo-federation/tests/query_plan/supergraphs/fragments_that_share_a_hash_but_are_not_identical_generate_their_own_fragment_definitions.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/fragments_that_share_a_hash_but_are_not_identical_generate_their_own_fragment_definitions.graphql @@ -1,16 +1,18 @@ -# Composed from subgraphs with hash: 9d07e44c7cffd48b0677fce186f5ba41a864bc13 +# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } directive @custom on FRAGMENT_SPREAD | INLINE_FRAGMENT +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) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,6 +39,8 @@ type B z: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql b/apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql index f43bbbed3d..9b7600e8a4 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 6655be753e0c804f83461f143965a01f3a040e1d +# Composed from subgraphs with hash: c2ea0958d4d930ae3bf3535692a966af9f3af063 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql b/apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql index 1e9922d407..3a1ae9fab2 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 3d0f2fed8651dd6a1c0d4b7bb3c94989781c8b68 +# Composed from subgraphs with hash: 2ad000b79369f5b833cec8d6e5e62e99db89585c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_case_of_key_chains_in_parallel_requires.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_case_of_key_chains_in_parallel_requires.graphql index fc9d503e9c..fd7953bfd3 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_case_of_key_chains_in_parallel_requires.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_case_of_key_chains_in_parallel_requires.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 31c0bf4aeea8c73b60a47e91ea9bcc475127782a +# Composed from subgraphs with hash: 6f7dd1a877d5de533c98948e91197cb3526ed446 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_fragments_with_interface_field_subtyping.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_fragments_with_interface_field_subtyping.graphql index 5bdeacfe85..98032aadc8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_fragments_with_interface_field_subtyping.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_fragments_with_interface_field_subtyping.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: ed9e25c79dbfc98582011fc534e6998d77ec556c +# Composed from subgraphs with hash: cd5671782860ad7353fd02dfb07664cdc89a5809 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ interface I other: I! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_mix_of_fragments_indirection_and_unions.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_mix_of_fragments_indirection_and_unions.graphql index 9ceaef8378..2ff8417e3b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_mix_of_fragments_indirection_and_unions.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_mix_of_fragments_indirection_and_unions.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 857413268aa91cd215bfa7f9a717a01afcb77048 +# Composed from subgraphs with hash: 0b61d90468ba75e031fdc3c1abe0ac8e87674cc7 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -39,6 +41,8 @@ type Child id: ID! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_multiple_conditions_on_abstract_types.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_conditions_on_abstract_types.graphql index 2965317df8..76280a249e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_multiple_conditions_on_abstract_types.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_conditions_on_abstract_types.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 1b8ed03436d12bd5fe1dc3487873cc027b7f81da +# Composed from subgraphs with hash: 2bb5218456c710b3aeaf9c5a9a7d87eee258917f schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -34,6 +36,8 @@ type Book implements Product reviews: [Review!]! @join__field(graph: REVIEWS) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql index 78d523b512..9a5c273ce0 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: de3bbad205ccca07c2a0fe699d358368241993b7 +# Composed from subgraphs with hash: 6120a9371b90c378d13d977d19bad9e17c6875cb schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -31,6 +33,8 @@ type Item computed2: String @join__field(graph: SUBGRAPH2, requires: "user { value }") } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_non_intersecting_fragment_conditions.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_non_intersecting_fragment_conditions.graphql index 134bb9a299..4be5ebe264 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_non_intersecting_fragment_conditions.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_non_intersecting_fragment_conditions.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 621fe405b7bc52f4f5f0102b2aaf8a374504eee3 +# Composed from subgraphs with hash: cc0d8d93561b6e20d87ae1541b16037be17333ef schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -42,6 +44,8 @@ interface Fruit edible: Boolean! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_non_matching_value_types_under_interface_field.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_non_matching_value_types_under_interface_field.graphql index 27487542e0..10cc68d004 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_non_matching_value_types_under_interface_field.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_non_matching_value_types_under_interface_field.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 688f2dad6c47c75df28f6cdd47bb6cc242311192 +# Composed from subgraphs with hash: 9959b044b8b84e47c4559354eb1d87299be28495 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -26,6 +28,8 @@ interface I s: S } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_query_of_an_interface_field_for_a_specific_implementation_when_query_starts_with_interface_object.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_query_of_an_interface_field_for_a_specific_implementation_when_query_starts_with_interface_object.graphql index 14f041319c..97189f9365 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_query_of_an_interface_field_for_a_specific_implementation_when_query_starts_with_interface_object.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_query_of_an_interface_field_for_a_specific_implementation_when_query_starts_with_interface_object.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 43bf934e8826f0b4d17867095ea2025fb352287a +# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I y: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_requires_from_supergraph.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_requires_from_supergraph.graphql index a3f47c437f..4afa0074ca 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_requires_from_supergraph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_requires_from_supergraph.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 46a2d6c6cf9956c08daa5b3faa018245cb5f9cfe +# Composed from subgraphs with hash: c980fbf8b4a77ab38f828719197c4878658aa92c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ interface I name: String } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_root_operation_shareable_in_many_subgraphs.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_root_operation_shareable_in_many_subgraphs.graphql index 76ccd6875f..9c2c5435dd 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_root_operation_shareable_in_many_subgraphs.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_root_operation_shareable_in_many_subgraphs.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 23796fabaf5b788d4a3df5cd4043263f2d825828 +# Composed from subgraphs with hash: dece71b67cd54bf5e3bf43988e99a638876d52e5 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_simple_requires.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_simple_requires.graphql index 2620fedb2f..a7bbfe095a 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_simple_requires.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_simple_requires.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: f4e377fc3c287e29c8acbbdb5614ae18542b310d +# Composed from subgraphs with hash: a2964ea427e664c7c007a258b54d68db9abae8f4 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_spread_unions_correctly.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_spread_unions_correctly.graphql index f5ffa85f3b..d0d026a6f3 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_spread_unions_correctly.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_spread_unions_correctly.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: cac8f01c24f23138d810fe086c24cf3e3c9b724e +# Composed from subgraphs with hash: 361121049969085d46eae818d8c9d8da18d3cf6c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -45,6 +47,8 @@ type C c2: Int @join__field(graph: SUBGRAPH2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_types_with_no_common_supertype_at_the_same_merge_at.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_types_with_no_common_supertype_at_the_same_merge_at.graphql index 7fd7b3ba28..15ae958653 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_types_with_no_common_supertype_at_the_same_merge_at.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_types_with_no_common_supertype_at_the_same_merge_at.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 914913e8f6d05467f239b8984f8875df35e9ec9c +# Composed from subgraphs with hash: 98f941b67e6dc18205c3148808a6bce2b64c775f schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -38,6 +40,8 @@ type Foo y: Int @join__field(graph: SUBGRAPH2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction.graphql b/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction.graphql index a1130db8d5..4c89d271df 100644 --- a/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: d0de8bd2759f8c48a97e2e09f5b1cd8bad7e6e78 +# Composed from subgraphs with hash: 6ac1ca5a8c7d1596024e97bb378f1f829788fd71 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -58,6 +60,8 @@ interface I2 v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction_but_no_need_to_type_explode.graphql b/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction_but_no_need_to_type_explode.graphql index 86b278865d..5433f33dcf 100644 --- a/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction_but_no_need_to_type_explode.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction_but_no_need_to_type_explode.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 7089406367e2cc6e6d0864f78d01407a184ffa92 +# Composed from subgraphs with hash: 5073ac23b2d2f5657028261e837e26e4370a3cf4 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -58,6 +60,8 @@ interface I2 v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction.graphql b/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction.graphql index fdd3f10953..11afc89b98 100644 --- a/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 34d04b226bb8991dcd8a063656dbf04b18555a0c +# Composed from subgraphs with hash: 43ca669a61b756dfac8bec5822d87393bc9f3544 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -48,6 +50,8 @@ interface I v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction_but_no_need_to_type_explode.graphql b/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction_but_no_need_to_type_explode.graphql index 996c6012dc..60c26dd523 100644 --- a/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction_but_no_need_to_type_explode.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction_but_no_need_to_type_explode.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 5c7d149e1ef1b997ab3395128beb7403f1f11dfe +# Composed from subgraphs with hash: 2548adb14fdc2eacf229834ee87327873c15cb8d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql b/apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql index 9cb1ecc9b4..fcbc7f2aa3 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 84c56a8f07d5763e8dfa886575ed2a5df80bbaf6 +# Composed from subgraphs with hash: 7225f76e0549cfa72d92ea3d954fbfa4f5325cff schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ interface I b: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_avoid_fragments_usable_only_once.graphql b/apollo-federation/tests/query_plan/supergraphs/it_avoid_fragments_usable_only_once.graphql index 809c44d072..629427d8e7 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_avoid_fragments_usable_only_once.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_avoid_fragments_usable_only_once.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 61f7aebc1284fd8dc840de895884ee5b7fd836c2 +# Composed from subgraphs with hash: 57cf5fd9ee4bd6e69d9976aef7ff289a74c757c4 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_with_lists.graphql b/apollo-federation/tests/query_plan/supergraphs/it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_with_lists.graphql index b18286ce84..b989cee25c 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_with_lists.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_with_lists.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 6509cee656ab4776fa9304fdc43ae726848fb0e0 +# Composed from subgraphs with hash: 885b71f276ed574d24895695bddb2bd130640f65 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -46,6 +48,8 @@ interface I expansiveField: String @join__field(graph: S1) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql index a4bcc63125..45bca01a72 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 78f8e2391131a01115d7025d6f7aae3763f91d5e +# Composed from subgraphs with hash: 49f8443a5ff323a1ffed6b7122a1d61e6225d235 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) { query: Query @@ -9,9 +9,11 @@ schema directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_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) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -23,6 +25,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_needlessly_consider_options_for_typename.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_needlessly_consider_options_for_typename.graphql index d4597c4e76..82876af15e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_does_not_needlessly_consider_options_for_typename.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_needlessly_consider_options_for_typename.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 147f9156540134dbaf7c6047530df577b0c28ced +# Composed from subgraphs with hash: ee7dda36e9dc663f9750f86da48822e8f6f6ea8f schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgraph.graphql index 7230a3c168..d0358af17b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgraph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgraph.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 7216df3d57d6ea85890cb4557bc1400d2a32faf0 +# Composed from subgraphs with hash: 11ffa462805ad28daecca693ed1a45d931d3cac8 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -28,6 +30,8 @@ interface I b: Int @join__field(graph: SUBGRAPH2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_executes_mutation_operations_in_sequence.graphql b/apollo-federation/tests/query_plan/supergraphs/it_executes_mutation_operations_in_sequence.graphql index 2dbf99dd61..c8a3b06e68 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_executes_mutation_operations_in_sequence.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_executes_mutation_operations_in_sequence.graphql @@ -1,15 +1,17 @@ -# Composed from subgraphs with hash: fea1be241ad3c07fc4036893012a86c4e17ea159 +# Composed from subgraphs with hash: ff6534336a58b9a72232e43fdb66c4769ac4ae66 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query mutation: Mutation } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -21,6 +23,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql index 1ef09945bd..b410e60027 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: c6a3588d5bfe556f3de3cf2f3fa2165704f9206d +# Composed from subgraphs with hash: 2a90ec602bc41c24462757dc66b2e37e1d96dbf4 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql index 49e3180b5b..c7102b37a9 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 0e4eac66a889724333f86a54a0647cc4f694f511 +# Composed from subgraphs with hash: a95f4fdeb4fa54d3e87c7a5eb71952eba3efdf28 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql index 49e3180b5b..c7102b37a9 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 0e4eac66a889724333f86a54a0647cc4f694f511 +# Composed from subgraphs with hash: a95f4fdeb4fa54d3e87c7a5eb71952eba3efdf28 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql index 020d3b8da0..1b13a0feb0 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 2ea9dba0e4a9f205821228485cdd112ca0b0c17a +# Composed from subgraphs with hash: b43c835356ccf4afa94c6ad48206c70f22f39ffc schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,6 +39,8 @@ type B c: Int @join__field(graph: SUBGRAPH3, requires: "required") } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql index 8a382fa437..8fbe10543a 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 722cd56dba6c8500f961ba2c48cc9f643b2fda98 +# Composed from subgraphs with hash: eeb0b0963b210e2f82e6f40f535037882afb96db schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -38,6 +40,8 @@ type Inner4Type inner4_nested: Int! @join__field(graph: SUBGRAPH5, requires: "inner4_required") } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs.graphql index 836e43cc0a..39f730f702 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 85ca2a0ec950344b1f685478ac0ea4f36e5b7692 +# Composed from subgraphs with hash: 0545139260d6549f4e32906bce43c37742fdff80 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -36,6 +38,8 @@ type Inner implements I w: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs.graphql index af1d65d24e..8706c2af1e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 532639290329813c32101df1b46a23c760db68fc +# Composed from subgraphs with hash: 2be5cb3476eaeac718ff9d77f717b70fe9f344a2 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type Inner w: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragments_with_one_non_leaf_field.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragments_with_one_non_leaf_field.graphql index eaf9c3e94d..a745ed6263 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragments_with_one_non_leaf_field.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragments_with_one_non_leaf_field.graphql @@ -1,16 +1,18 @@ -# Composed from subgraphs with hash: 9d07e44c7cffd48b0677fce186f5ba41a864bc13 +# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } directive @custom on FRAGMENT_SPREAD | INLINE_FRAGMENT +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) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,6 +39,8 @@ type B z: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_in_nested_entity.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_in_nested_entity.graphql index 9958fad35f..fe5bef73a1 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_in_nested_entity.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_in_nested_entity.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: e8059ef7894398e0a461b58b839372a8b3a61d64 +# Composed from subgraphs with hash: 3881ebaea69bd73941780a88cffefd7f909c77d1 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -47,6 +49,8 @@ interface I a: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_input_rewrites_when_cloning_dependency_graph.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_input_rewrites_when_cloning_dependency_graph.graphql index 398d4b25e2..41b8deead2 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_input_rewrites_when_cloning_dependency_graph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_input_rewrites_when_cloning_dependency_graph.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: ebabfd0be0356580cd148a45b86e2db82b29d3c3 +# Composed from subgraphs with hash: 3799eb12e6387413ef90a73d8848b5c48e40cbca schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,6 +31,8 @@ interface I i3: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql index 207d9de6f3..0d81bb4ee6 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 125a036421e2f6b9aa7e2c1bcabc29a4e3aa32cf +# Composed from subgraphs with hash: 00470b1d89f783a11f9b05f82d8377c0d5e3f89d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_with_multiple_fetches.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_with_multiple_fetches.graphql index 8e4d26a88e..57e31110e1 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_with_multiple_fetches.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_with_multiple_fetches.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 461e4a611a1faf2558d6ee6e3de4af24a043fc16 +# Composed from subgraphs with hash: 55b2cd6d674b743ae99fa918dce065759899f51d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,6 +31,8 @@ interface I name: String! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql index e598d84a1b..8beeb189bd 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: dd69cd7ccb9070ab8a3de08ead3e41eacce180d1 +# Composed from subgraphs with hash: 151440cab35934f002ebab673ce5f8bd86966772 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -28,6 +30,8 @@ interface I g: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation.graphql index eaf9c3e94d..a745ed6263 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation.graphql @@ -1,16 +1,18 @@ -# Composed from subgraphs with hash: 9d07e44c7cffd48b0677fce186f5ba41a864bc13 +# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } directive @custom on FRAGMENT_SPREAD | INLINE_FRAGMENT +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) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,6 +39,8 @@ type B z: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql index 251390eef3..3d4964b712 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: fadb16531ae6d8fe34df35cd8993071bd83b8160 +# Composed from subgraphs with hash: ff92849ceafa9aaccab960b8b5ce0e98a13e6a00 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_requires_on_concrete_type_of_field_provided_by_interface_object.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_requires_on_concrete_type_of_field_provided_by_interface_object.graphql index b0306ba46d..aae7fd038a 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_requires_on_concrete_type_of_field_provided_by_interface_object.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_requires_on_concrete_type_of_field_provided_by_interface_object.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: c6d2827cd86e25fe8093045c7a65ad35e1ca5101 +# Composed from subgraphs with hash: 209a0436ca69cb640450ef4fc7c204a5b5fc6058 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -45,6 +47,8 @@ interface I x: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql index 15556cf8e9..b076bc92fe 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 49a33e65faa01be4bcd2f444dd5f10f439a286a8 +# Composed from subgraphs with hash: 244af51d2d8ba7d87e4b9fec74bbb49dc7b00e4d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_identifies_and_reuses_equivalent_fragments_that_arent_identical.graphql b/apollo-federation/tests/query_plan/supergraphs/it_identifies_and_reuses_equivalent_fragments_that_arent_identical.graphql index eaf9c3e94d..a745ed6263 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_identifies_and_reuses_equivalent_fragments_that_arent_identical.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_identifies_and_reuses_equivalent_fragments_that_arent_identical.graphql @@ -1,16 +1,18 @@ -# Composed from subgraphs with hash: 9d07e44c7cffd48b0677fce186f5ba41a864bc13 +# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } directive @custom on FRAGMENT_SPREAD | INLINE_FRAGMENT +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) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,6 +39,8 @@ type B z: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_migrates_skip_include.graphql b/apollo-federation/tests/query_plan/supergraphs/it_migrates_skip_include.graphql index eaf9c3e94d..a745ed6263 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_migrates_skip_include.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_migrates_skip_include.graphql @@ -1,16 +1,18 @@ -# Composed from subgraphs with hash: 9d07e44c7cffd48b0677fce186f5ba41a864bc13 +# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } directive @custom on FRAGMENT_SPREAD | INLINE_FRAGMENT +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) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,6 +39,8 @@ type B z: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preservers_aliased_typename.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preservers_aliased_typename.graphql index 4a98373896..adc483ebe7 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_preservers_aliased_typename.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_preservers_aliased_typename.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: b5d9704c92902a18f288f76f368385798d68f1f1 +# Composed from subgraphs with hash: d7fe5a716fee436faefb289f7ba4a8bd05bd7d34 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_is_reused.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_is_reused.graphql index d6ca67b84f..9aa130fc7f 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_is_reused.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_is_reused.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: b6ebc4ffa76637f33269672e1a49739b3b7091a8 +# Composed from subgraphs with hash: 75955b009750aae84f92b194eddcb553f0e44656 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_not_used.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_not_used.graphql index d6ca67b84f..9aa130fc7f 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_not_used.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_not_used.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: b6ebc4ffa76637f33269672e1a49739b3b7091a8 +# Composed from subgraphs with hash: 75955b009750aae84f92b194eddcb553f0e44656 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preserves_nested_fragments_when_outer_one_has_directives_and_is_eliminated.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preserves_nested_fragments_when_outer_one_has_directives_and_is_eliminated.graphql index fd0936744c..a12329a27d 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_preserves_nested_fragments_when_outer_one_has_directives_and_is_eliminated.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_preserves_nested_fragments_when_outer_one_has_directives_and_is_eliminated.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 2bb9de0177faf2f6dbf5fe84f5dd6e07fbc61635 +# Composed from subgraphs with hash: f580fdce2d5df285d6e12633d4355b51e2fd52fd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql b/apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql index 44f27a7576..66be42e56a 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 57efb2956792ca0eb40a9df16cbd08aad77cfd44 +# Composed from subgraphs with hash: 3766e23446d1f1881fb155fefa8036726c6f34c4 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_respects_generate_query_fragments_option.graphql b/apollo-federation/tests/query_plan/supergraphs/it_respects_generate_query_fragments_option.graphql index eaf9c3e94d..a745ed6263 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_respects_generate_query_fragments_option.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_respects_generate_query_fragments_option.graphql @@ -1,16 +1,18 @@ -# Composed from subgraphs with hash: 9d07e44c7cffd48b0677fce186f5ba41a864bc13 +# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } directive @custom on FRAGMENT_SPREAD | INLINE_FRAGMENT +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) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,6 +39,8 @@ type B z: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql index fabd270b4b..a7d3b59025 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 8f950745a224d72bf86dc6d2c6a66c76ed7c5fe0 +# Composed from subgraphs with hash: d654244e34da1e73ea47f5325e371614408d38ae schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -26,6 +28,8 @@ interface I v: Value } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql index c92e397384..bb44229f8d 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 627a6f4e651e7ff691b6c7d4104a68dd562dd7b7 +# Composed from subgraphs with hash: 87210bff4bf720ff0fe68c22dae1d3e5520d7980 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_1.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_1.graphql index 47f05c483a..3b5fe2a129 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_1.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_1.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: e0dfa4222558f28e0ecb3e26077458471e69267b +# Composed from subgraphs with hash: 18fc379a3170731963d1ec9f54f8b002b0f5d874 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -62,6 +64,8 @@ interface Foo child2: Foo } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved.graphql index 64e2fd6e3b..ef1c275632 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 425c9d41052b9208347cddf5826dfcaed779900a +# Composed from subgraphs with hash: 197c4ea5c04d17ec60cb084c49efe2f9d662079b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql index bc8611b65a..581e7c764b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 952c14738ea71875811bacb2c3fdae24798ea16b +# Composed from subgraphs with hash: fb1513832764f051dd663a966309809fb58e4519 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql index eb282becb9..1b20db25a4 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 3c2e16964e0f336b33ea0c717a1a45a4f11e6596 +# Composed from subgraphs with hash: 0556f39f18edcd446ffee2af4cec39d408bf7b7d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -35,6 +37,8 @@ interface I a: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/key_where_at_external_is_not_at_top_level_of_selection_of_requires.graphql b/apollo-federation/tests/query_plan/supergraphs/key_where_at_external_is_not_at_top_level_of_selection_of_requires.graphql index b35c49754a..c5c7c136a3 100644 --- a/apollo-federation/tests/query_plan/supergraphs/key_where_at_external_is_not_at_top_level_of_selection_of_requires.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/key_where_at_external_is_not_at_top_level_of_selection_of_requires.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: c1a3645bcd1d5de8ffaaafc2db246bb829597152 +# Composed from subgraphs with hash: 3035e486a3ec94c91ff841c2eb464b287fead177 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_order.graphql b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_order.graphql index 9447973fc1..9d3b36f76a 100644 --- a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_order.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_order.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 58185941858c72bbf45ca3d14674013b6a6c0ce7 +# Composed from subgraphs with hash: 1cce0a8dc674e651027f8d368504edb388f9b239 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type Hello goodbye: String! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_quantity.graphql b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_quantity.graphql index bf44f896c5..80f6562f18 100644 --- a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_quantity.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_quantity.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: f4972a81ae6443a316826d336e1947a5aad4d2e6 +# Composed from subgraphs with hash: fbbe470b5e40af130de42043f74772ec30ee60ff schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type Hello goodbye: String! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_identical.graphql b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_identical.graphql index bf44f896c5..80f6562f18 100644 --- a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_identical.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_identical.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: f4972a81ae6443a316826d336e1947a5aad4d2e6 +# Composed from subgraphs with hash: fbbe470b5e40af130de42043f74772ec30ee60ff schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type Hello goodbye: String! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_with_fragment.graphql b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_with_fragment.graphql index bf44f896c5..80f6562f18 100644 --- a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_with_fragment.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_with_fragment.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: f4972a81ae6443a316826d336e1947a5aad4d2e6 +# Composed from subgraphs with hash: fbbe470b5e40af130de42043f74772ec30ee60ff schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type Hello goodbye: String! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_without_fragment.graphql b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_without_fragment.graphql index bf44f896c5..80f6562f18 100644 --- a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_without_fragment.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_without_fragment.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: f4972a81ae6443a316826d336e1947a5aad4d2e6 +# Composed from subgraphs with hash: fbbe470b5e40af130de42043f74772ec30ee60ff schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type Hello goodbye: String! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/multiplication_overflow_in_reduce_options_if_needed.graphql b/apollo-federation/tests/query_plan/supergraphs/multiplication_overflow_in_reduce_options_if_needed.graphql index 0b6baf2e68..42f3b4b125 100644 --- a/apollo-federation/tests/query_plan/supergraphs/multiplication_overflow_in_reduce_options_if_needed.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/multiplication_overflow_in_reduce_options_if_needed.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: bf831e2e6890f60e5c8e93bc52ce549323cb23e8 +# Composed from subgraphs with hash: 91d4d0661d413b60ae2ed463c074c4180d7b849e schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/nested_fragment_with_interseting_parent_type_and_directive_condition.graphql b/apollo-federation/tests/query_plan/supergraphs/nested_fragment_with_interseting_parent_type_and_directive_condition.graphql index 4417e0ea0b..1e2a373495 100644 --- a/apollo-federation/tests/query_plan/supergraphs/nested_fragment_with_interseting_parent_type_and_directive_condition.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/nested_fragment_with_interseting_parent_type_and_directive_condition.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 059bed56ce74dbb40643a1561b79514872f0ab60 +# Composed from subgraphs with hash: e2393609250b71261acc4089006bc8a14627d488 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -35,6 +37,8 @@ interface I2 title: String } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/non_adjacent_mutations_do_not_get_merged.graphql b/apollo-federation/tests/query_plan/supergraphs/non_adjacent_mutations_do_not_get_merged.graphql index e99930397b..ae84cc8ed8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/non_adjacent_mutations_do_not_get_merged.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/non_adjacent_mutations_do_not_get_merged.graphql @@ -1,15 +1,17 @@ -# Composed from subgraphs with hash: 2655e7da6754e73955fece01d7cbb5f21085bdbb +# Composed from subgraphs with hash: 54adb76945715a2ce0e068d2635d71ca4166b289 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query mutation: Mutation } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -30,6 +32,8 @@ type Foo baz: Int @join__field(graph: SUBGRAPHB) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/only_uses_an_interface_object_if_it_can.graphql b/apollo-federation/tests/query_plan/supergraphs/only_uses_an_interface_object_if_it_can.graphql index 14f041319c..97189f9365 100644 --- a/apollo-federation/tests/query_plan/supergraphs/only_uses_an_interface_object_if_it_can.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/only_uses_an_interface_object_if_it_can.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 43bf934e8826f0b4d17867095ea2025fb352287a +# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I y: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql b/apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql index 74a64a0313..dc51cbcdcc 100644 --- a/apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 87d7f4f15c5d3342bb4073f6926e35ba9d0cf435 +# Composed from subgraphs with hash: 454b3fc41adce04fc670f06030743d521d09096d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -35,6 +37,8 @@ type Currency sign: String! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/redundant_typename_for_inline_fragments_without_type_condition.graphql b/apollo-federation/tests/query_plan/supergraphs/redundant_typename_for_inline_fragments_without_type_condition.graphql index 0b9050d640..923d850187 100644 --- a/apollo-federation/tests/query_plan/supergraphs/redundant_typename_for_inline_fragments_without_type_condition.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/redundant_typename_for_inline_fragments_without_type_condition.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: d267b3f18f7736e1a3e39a951696c318d1f46a34 +# Composed from subgraphs with hash: f02b27ab5f92e1e70c07bf7d5ce65e620637ef35 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_false.graphql b/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_false.graphql index 42f3923d30..003c7fc0ec 100644 --- a/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_false.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_false.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: efdbea0791d6ac8577eb1b7b63618adec547f1c8 +# Composed from subgraphs with hash: 4fc759c1b39c54d520a6868db43813e4a38bbaf3 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type A y: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_true.graphql b/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_true.graphql index 42f3923d30..003c7fc0ec 100644 --- a/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_true.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_true.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: efdbea0791d6ac8577eb1b7b63618adec547f1c8 +# Composed from subgraphs with hash: 4fc759c1b39c54d520a6868db43813e4a38bbaf3 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type A y: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql b/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql index a6d3125d50..6c750cf146 100644 --- a/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: d5699e8f9c56867b4a27e8b7b2e942a65af9c97b +# Composed from subgraphs with hash: a3e748206fa553b8f317912a3a411ee68b55db5d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -36,6 +38,8 @@ type Foo bar: Bar } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/skip_type_explosion_early_if_unnecessary.graphql b/apollo-federation/tests/query_plan/supergraphs/skip_type_explosion_early_if_unnecessary.graphql index 2626014b39..c0db4184d4 100644 --- a/apollo-federation/tests/query_plan/supergraphs/skip_type_explosion_early_if_unnecessary.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/skip_type_explosion_early_if_unnecessary.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 92a7f0921bb3da20d67bc49b77d12eecef437e91 +# Composed from subgraphs with hash: 63eb1eb88aa472162170c24e74ca47c7c4ac1866 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -26,6 +28,8 @@ interface I s: S } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/subgraph_query_retains_the_query_variables_used_in_the_directives_applied_to_the_query.graphql b/apollo-federation/tests/query_plan/supergraphs/subgraph_query_retains_the_query_variables_used_in_the_directives_applied_to_the_query.graphql index 13112ebd1d..67fa096ee1 100644 --- a/apollo-federation/tests/query_plan/supergraphs/subgraph_query_retains_the_query_variables_used_in_the_directives_applied_to_the_query.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/subgraph_query_retains_the_query_variables_used_in_the_directives_applied_to_the_query.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 97c42b7140502842f2f75aed633e82397ef462c4 +# Composed from subgraphs with hash: 64759e825a65c99c54fedcbf511cc7bf0735d4e2 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,6 +24,8 @@ directive @link(url: String, as: String, for: link__Purpose, import: [link__Impo directive @withArgs(arg1: String) on QUERY +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_at_the_operation_level_are_passed_down_to_subgraph_queries.graphql b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_at_the_operation_level_are_passed_down_to_subgraph_queries.graphql index 25a1e12239..f2012f3f04 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_at_the_operation_level_are_passed_down_to_subgraph_queries.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_at_the_operation_level_are_passed_down_to_subgraph_queries.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 0574a22aea40ebfdb91c4ea931889f285c43a04c +# Composed from subgraphs with hash: a0524dbe1bbd3a7450a2e15f5c25c5cf2eed4242 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query mutation: Mutation @@ -9,9 +9,11 @@ schema directive @field on FIELD +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) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -35,6 +37,8 @@ type Foo baz: Int @join__field(graph: SUBGRAPHB) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_on_mutations_are_passed_down_to_subgraph_queries.graphql b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_on_mutations_are_passed_down_to_subgraph_queries.graphql index 25a1e12239..f2012f3f04 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_on_mutations_are_passed_down_to_subgraph_queries.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_on_mutations_are_passed_down_to_subgraph_queries.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 0574a22aea40ebfdb91c4ea931889f285c43a04c +# Composed from subgraphs with hash: a0524dbe1bbd3a7450a2e15f5c25c5cf2eed4242 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query mutation: Mutation @@ -9,9 +9,11 @@ schema directive @field on FIELD +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) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -35,6 +37,8 @@ type Foo baz: Int @join__field(graph: SUBGRAPHB) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_with_arguments_applied_on_queries_are_ok.graphql b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_with_arguments_applied_on_queries_are_ok.graphql index c59e350fed..5eb84bed66 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_with_arguments_applied_on_queries_are_ok.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_with_arguments_applied_on_queries_are_ok.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 20fe4c183c4983750398bd0be297f8ddc4251006 +# Composed from subgraphs with hash: 5338f8c604ab7c2103bdf58ee6752a40d4b62ed8 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -24,6 +26,8 @@ directive @noArgs on QUERY directive @withArgs(arg1: String) on QUERY +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_do_not_create_cycle_in_fetch_dependency_graph.graphql b/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_do_not_create_cycle_in_fetch_dependency_graph.graphql index aea8d395a8..82e69229f1 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_do_not_create_cycle_in_fetch_dependency_graph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_do_not_create_cycle_in_fetch_dependency_graph.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 58cfa42df5c5f20fb0fbe43d4a506b3654439de1 +# Composed from subgraphs with hash: 0b567f1d7e0089a146e8499a2c5e412b78a98498 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_reset_cached_costs.graphql b/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_reset_cached_costs.graphql index c513bf6ad9..6b66fcbf6e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_reset_cached_costs.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_reset_cached_costs.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 30529f3ff76bc917d12a637ede6e78ce39e7bca8 +# Composed from subgraphs with hash: 7c07647747a84fd6c839bb603948dddcadf654fd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/trying_to_use_defer_with_a_subcription_results_in_an_error.graphql b/apollo-federation/tests/query_plan/supergraphs/trying_to_use_defer_with_a_subcription_results_in_an_error.graphql index 25f9d7d739..1d901c0ee1 100644 --- a/apollo-federation/tests/query_plan/supergraphs/trying_to_use_defer_with_a_subcription_results_in_an_error.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/trying_to_use_defer_with_a_subcription_results_in_an_error.graphql @@ -1,15 +1,17 @@ -# Composed from subgraphs with hash: 010102349764cd3dac7215c8cc5c825916b3b8db +# Composed from subgraphs with hash: 4c8b155cb8183b493b5c2862a2bd4ce0261642f9 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query subscription: Subscription } +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) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -21,6 +23,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction.graphql b/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction.graphql index ccbe1edfa3..347f1bcfb8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 9355e80c20445d59cd2d9a8cd04db6a6bebb3ce0 +# Composed from subgraphs with hash: 336400b353f835910c178a10eb77371f8700396b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction_but_no_need_to_type_explode.graphql b/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction_but_no_need_to_type_explode.graphql index 79d0e203e2..3dfc5d39b8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction_but_no_need_to_type_explode.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction_but_no_need_to_type_explode.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 16225ee3e1f3aaff3e1fe3e4cfe3298d8095f608 +# Composed from subgraphs with hash: 0457b8e67ae0ea99ead4c13318c4ac89e821aac3 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -48,6 +50,8 @@ interface I v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/union_union_interaction.graphql b/apollo-federation/tests/query_plan/supergraphs/union_union_interaction.graphql index 35a5d9f1c7..bb7bf23e98 100644 --- a/apollo-federation/tests/query_plan/supergraphs/union_union_interaction.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/union_union_interaction.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 0fdc118d2b5e2aed1f67357115b1614c2a230888 +# Composed from subgraphs with hash: 997336a5c7996069d8c10d0b9be46a72b46459c1 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -39,6 +41,8 @@ type C v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/union_union_interaction_but_no_need_to_type_explode.graphql b/apollo-federation/tests/query_plan/supergraphs/union_union_interaction_but_no_need_to_type_explode.graphql index 4a4292e66c..3dd79adf66 100644 --- a/apollo-federation/tests/query_plan/supergraphs/union_union_interaction_but_no_need_to_type_explode.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/union_union_interaction_but_no_need_to_type_explode.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 03fe71c2f384dc3668709b401037d4139e6cd4e5 +# Composed from subgraphs with hash: 241b1f7314833f7c2a97da8176e258c40322b0d7 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -39,6 +41,8 @@ type C v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql b/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql index bed3adbc67..a3668cb67b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 07faf0f54a80eb7ef3e78f4fbe6382e135f6aac6 +# Composed from subgraphs with hash: 133c359820bd42cb8afd64ab1e02bb912b5d1746 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -35,6 +37,8 @@ type Foo bar: Bar } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/works_when_unset.graphql b/apollo-federation/tests/query_plan/supergraphs/works_when_unset.graphql index 0b6baf2e68..42f3b4b125 100644 --- a/apollo-federation/tests/query_plan/supergraphs/works_when_unset.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/works_when_unset.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: bf831e2e6890f60e5c8e93bc52ce549323cb23e8 +# Composed from subgraphs with hash: 91d4d0661d413b60ae2ed463c074c4180d7b849e schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/works_with_key_chains.graphql b/apollo-federation/tests/query_plan/supergraphs/works_with_key_chains.graphql index 51ca7bb320..48816b3a72 100644 --- a/apollo-federation/tests/query_plan/supergraphs/works_with_key_chains.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/works_with_key_chains.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: a5757c4964ac9dabd9a7dbd22a30a62f5d863033 +# Composed from subgraphs with hash: 4387e2f917c7748296b92799227648747e453bec schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { From efb752c3550420736462ac3c68d1f23e4f41d275 Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:36:24 -0500 Subject: [PATCH 33/56] feat(federation): add support for progressive overrides (#6011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement support for progressive `@override`s (https://github.com/apollographql/federation/pull/2902). Co-authored-by: Renée --- apollo-federation/cli/src/bench.rs | 2 +- apollo-federation/cli/src/main.rs | 5 +- apollo-federation/src/error/mod.rs | 2 - .../src/link/federation_spec_definition.rs | 32 ++ .../src/query_graph/build_query_graph.rs | 106 +++++ .../src/query_graph/graph_path.rs | 89 ++++- apollo-federation/src/query_graph/mod.rs | 64 ++- .../src/query_plan/query_planner.rs | 121 +++--- .../query_plan/query_planning_traversal.rs | 9 +- .../query_plan/build_query_plan_support.rs | 14 +- .../query_plan/build_query_plan_tests.rs | 2 +- .../fetch_operation_names.rs | 4 +- .../build_query_plan_tests/overrides.rs | 375 ++++++++++++++++++ .../overrides/shareable.rs | 244 ++++++++++++ ...ride_unset_labels_on_entity_fields.graphql | 73 ++++ ...set_labels_on_nested_entity_fields.graphql | 73 ++++ ...erride_unset_labels_on_root_fields.graphql | 53 +++ ...gressive_override_on_entity_fields.graphql | 73 ++++ ...e_override_on_nested_entity_fields.graphql | 73 ++++ ...rogressive_override_on_root_fields.graphql | 53 +++ ...es_f1_to_s3_when_label_is_provided.graphql | 66 +++ ...rides_to_s2_when_label_is_provided.graphql | 66 +++ ...1_in_s1_when_label_is_not_provided.graphql | 66 +++ ...s_in_s1_when_label_is_not_provided.graphql | 66 +++ .../cost_calculator/static_cost.rs | 4 +- .../src/query_planner/bridge_query_planner.rs | 40 +- .../src/query_planner/dual_query_planner.rs | 6 +- .../tests/integration/query_planner.rs | 106 ----- 28 files changed, 1657 insertions(+), 230 deletions(-) create mode 100644 apollo-federation/tests/query_plan/build_query_plan_tests/overrides.rs create mode 100644 apollo-federation/tests/query_plan/build_query_plan_tests/overrides/shareable.rs create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql diff --git a/apollo-federation/cli/src/bench.rs b/apollo-federation/cli/src/bench.rs index c672137982..ed135d8ba4 100644 --- a/apollo-federation/cli/src/bench.rs +++ b/apollo-federation/cli/src/bench.rs @@ -54,7 +54,7 @@ pub(crate) fn run_bench( } }; let now = Instant::now(); - let plan = planner.build_query_plan(&document, None); + let plan = planner.build_query_plan(&document, None, Default::default()); let elapsed = now.elapsed().as_secs_f64() * 1000.0; let mut eval_plans = None; let mut error = None; diff --git a/apollo-federation/cli/src/main.rs b/apollo-federation/cli/src/main.rs index ab42f16151..28bb5f7921 100644 --- a/apollo-federation/cli/src/main.rs +++ b/apollo-federation/cli/src/main.rs @@ -254,7 +254,10 @@ fn cmd_plan( let query_doc = ExecutableDocument::parse_and_validate(planner.api_schema().schema(), query, query_path)?; - print!("{}", planner.build_query_plan(&query_doc, None)?); + print!( + "{}", + planner.build_query_plan(&query_doc, None, Default::default())? + ); Ok(()) } diff --git a/apollo-federation/src/error/mod.rs b/apollo-federation/src/error/mod.rs index 9e4487c30f..d56c9783a6 100644 --- a/apollo-federation/src/error/mod.rs +++ b/apollo-federation/src/error/mod.rs @@ -33,8 +33,6 @@ impl From for String { #[derive(Clone, Debug, strum_macros::Display, PartialEq, Eq)] pub enum UnsupportedFeatureKind { - #[strum(to_string = "progressive overrides")] - ProgressiveOverrides, #[strum(to_string = "defer")] Defer, #[strum(to_string = "context")] diff --git a/apollo-federation/src/link/federation_spec_definition.rs b/apollo-federation/src/link/federation_spec_definition.rs index 67f181ec8b..184e93b690 100644 --- a/apollo-federation/src/link/federation_spec_definition.rs +++ b/apollo-federation/src/link/federation_spec_definition.rs @@ -12,6 +12,7 @@ use lazy_static::lazy_static; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::link::argument::directive_optional_boolean_argument; +use crate::link::argument::directive_optional_string_argument; use crate::link::argument::directive_required_string_argument; use crate::link::cost_spec_definition::CostSpecDefinition; use crate::link::cost_spec_definition::COST_VERSIONS; @@ -51,6 +52,11 @@ pub(crate) struct ProvidesDirectiveArguments<'doc> { pub(crate) fields: &'doc str, } +pub(crate) struct OverrideDirectiveArguments<'doc> { + pub(crate) from: &'doc str, + pub(crate) label: Option<&'doc str>, +} + #[derive(Debug)] pub(crate) struct FederationSpecDefinition { url: Url, @@ -361,6 +367,19 @@ impl FederationSpecDefinition { }) } + pub(crate) fn override_directive_definition<'schema>( + &self, + schema: &'schema FederationSchema, + ) -> Result<&'schema Node, FederationError> { + self.directive_definition(schema, &FEDERATION_OVERRIDE_DIRECTIVE_NAME_IN_SPEC)? + .ok_or_else(|| { + FederationError::internal(format!( + "Unexpectedly could not find federation spec's \"@{}\" directive definition", + FEDERATION_OVERRIDE_DIRECTIVE_NAME_IN_SPEC + )) + }) + } + pub(crate) fn override_directive( &self, schema: &FederationSchema, @@ -390,6 +409,19 @@ impl FederationSpecDefinition { }) } + pub(crate) fn override_directive_arguments<'doc>( + &self, + application: &'doc Node, + ) -> Result, FederationError> { + Ok(OverrideDirectiveArguments { + from: directive_required_string_argument(application, &FEDERATION_FROM_ARGUMENT_NAME)?, + label: directive_optional_string_argument( + application, + &FEDERATION_OVERRIDE_LABEL_ARGUMENT_NAME, + )?, + }) + } + pub(crate) fn get_cost_spec_definition( &self, schema: &FederationSchema, diff --git a/apollo-federation/src/query_graph/build_query_graph.rs b/apollo-federation/src/query_graph/build_query_graph.rs index 3dd7abbcd6..d4d2acf609 100644 --- a/apollo-federation/src/query_graph/build_query_graph.rs +++ b/apollo-federation/src/query_graph/build_query_graph.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use apollo_compiler::collections::HashMap; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::schema::DirectiveList as ComponentDirectiveList; @@ -21,6 +22,7 @@ use crate::link::federation_spec_definition::KeyDirectiveArguments; use crate::operation::merge_selection_sets; use crate::operation::Selection; use crate::operation::SelectionSet; +use crate::query_graph::OverrideCondition; use crate::query_graph::QueryGraph; use crate::query_graph::QueryGraphEdge; use crate::query_graph::QueryGraphEdgeTransition; @@ -140,6 +142,7 @@ impl BaseQueryGraphBuilder { QueryGraphEdge { transition, conditions, + override_condition: None, }, ); let head_weight = self.query_graph.node_weight(head)?; @@ -982,6 +985,7 @@ impl FederatedQueryGraphBuilder { self.add_root_edges()?; self.handle_key()?; self.handle_requires()?; + self.handle_progressive_overrides()?; // Note that @provides must be handled last when building since it requires copying nodes // and their edges, and it's easier to reason about this if we know previous self.handle_provides()?; @@ -1374,6 +1378,102 @@ impl FederatedQueryGraphBuilder { Ok(()) } + /// Handling progressive overrides here. For each progressive @override + /// application (with a label), we want to update the edges to the overridden + /// field within the "to" and "from" subgraphs with their respective override + /// condition (the label and a T/F value). The "from" subgraph will have an + /// override condition of `false`, whereas the "to" subgraph will have an + /// override condition of `true`. + fn handle_progressive_overrides(&mut self) -> Result<(), FederationError> { + let mut edge_to_conditions: HashMap = Default::default(); + + fn collect_edge_condition( + query_graph: &QueryGraph, + target_graph: &str, + target_field: &ObjectFieldDefinitionPosition, + label: &str, + condition: bool, + edge_to_conditions: &mut HashMap, + ) -> Result<(), FederationError> { + let target_field = FieldDefinitionPosition::Object(target_field.clone()); + let subgraph_nodes = query_graph + .types_to_nodes_by_source + .get(target_graph) + .unwrap(); + let parent_node = subgraph_nodes + .get(target_field.type_name()) + .unwrap() + .first() + .unwrap(); + for edge in query_graph.out_edges(*parent_node) { + let edge_weight = query_graph.edge_weight(edge.id())?; + let QueryGraphEdgeTransition::FieldCollection { + field_definition_position, + .. + } = &edge_weight.transition + else { + continue; + }; + + if &target_field == field_definition_position { + edge_to_conditions.insert( + edge.id(), + OverrideCondition { + label: label.to_string(), + condition, + }, + ); + } + } + Ok(()) + } + + for (to_subgraph_name, subgraph) in &self.base.query_graph.subgraphs_by_name { + let subgraph_data = self.subgraphs.get(to_subgraph_name)?; + if let Some(override_referencers) = subgraph + .referencers() + .directives + .get(&subgraph_data.overrides_directive_definition_name) + { + for field_definition_position in &override_referencers.object_fields { + let field = field_definition_position.get(subgraph.schema())?; + for directive in field + .directives + .get_all(&subgraph_data.overrides_directive_definition_name) + { + let application = subgraph_data + .federation_spec_definition + .override_directive_arguments(directive)?; + if let Some(label) = application.label { + collect_edge_condition( + &self.base.query_graph, + to_subgraph_name, + field_definition_position, + label, + true, + &mut edge_to_conditions, + )?; + collect_edge_condition( + &self.base.query_graph, + application.from, + field_definition_position, + label, + false, + &mut edge_to_conditions, + )?; + } + } + } + } + } + + for (edge, condition) in edge_to_conditions { + let mutable_edge = self.base.query_graph.edge_weight_mut(edge)?; + mutable_edge.override_condition = Some(condition); + } + Ok(()) + } + /// Handle @provides by copying the appropriate nodes/edges. fn handle_provides(&mut self) -> Result<(), FederationError> { let mut provide_id = 0; @@ -1987,6 +2087,10 @@ impl FederatedQueryGraphBuilderSubgraphs { ), } })?; + let overrides_directive_definition_name = federation_spec_definition + .override_directive_definition(schema)? + .name + .clone(); subgraphs.map.insert( source.clone(), FederatedQueryGraphBuilderSubgraphData { @@ -1995,6 +2099,7 @@ impl FederatedQueryGraphBuilderSubgraphs { requires_directive_definition_name, provides_directive_definition_name, interface_object_directive_definition_name, + overrides_directive_definition_name, }, ); } @@ -2020,6 +2125,7 @@ struct FederatedQueryGraphBuilderSubgraphData { requires_directive_definition_name: Name, provides_directive_definition_name: Name, interface_object_directive_definition_name: Name, + overrides_directive_definition_name: Name, } #[derive(Debug)] diff --git a/apollo-federation/src/query_graph/graph_path.rs b/apollo-federation/src/query_graph/graph_path.rs index c588f39d7c..88bba9e8a0 100644 --- a/apollo-federation/src/query_graph/graph_path.rs +++ b/apollo-federation/src/query_graph/graph_path.rs @@ -46,6 +46,7 @@ use crate::query_graph::path_tree::OpPathTree; use crate::query_graph::QueryGraph; use crate::query_graph::QueryGraphEdgeTransition; use crate::query_graph::QueryGraphNodeType; +use crate::query_plan::query_planner::EnabledOverrideConditions; use crate::query_plan::FetchDataPathElement; use crate::query_plan::QueryPathElement; use crate::query_plan::QueryPlanCost; @@ -1406,17 +1407,24 @@ where feature = "snapshot_tracing", tracing::instrument(skip_all, level = "trace") )] + #[allow(clippy::too_many_arguments)] fn advance_with_non_collecting_and_type_preserving_transitions( self: &Arc, context: &OpGraphPathContext, condition_resolver: &mut impl ConditionResolver, excluded_destinations: &ExcludedDestinations, excluded_conditions: &ExcludedConditions, + override_conditions: &EnabledOverrideConditions, transition_and_context_to_trigger: impl Fn( &QueryGraphEdgeTransition, &OpGraphPathContext, ) -> TTrigger, - node_and_trigger_to_edge: impl Fn(&Arc, NodeIndex, &Arc) -> Option, + node_and_trigger_to_edge: impl Fn( + &Arc, + NodeIndex, + &Arc, + &EnabledOverrideConditions, + ) -> Option, ) -> Result, FederationError> { // If we're asked for indirect paths after an "@interfaceObject fake down cast" but that // down cast comes just after non-collecting edge(s), then we can ignore the ask (skip @@ -1675,6 +1683,7 @@ where direct_path_start_node, edge_tail_type_pos, &node_and_trigger_to_edge, + override_conditions, )? } else { None @@ -1797,12 +1806,20 @@ where start_index: usize, start_node: NodeIndex, end_type_position: &OutputTypeDefinitionPosition, - node_and_trigger_to_edge: impl Fn(&Arc, NodeIndex, &Arc) -> Option, + node_and_trigger_to_edge: impl Fn( + &Arc, + NodeIndex, + &Arc, + &EnabledOverrideConditions, + ) -> Option, + override_conditions: &EnabledOverrideConditions, ) -> Result, FederationError> { let mut current_node = start_node; for index in start_index..self.edges.len() { let trigger = &self.edge_triggers[index]; - let Some(edge) = node_and_trigger_to_edge(&self.graph, current_node, trigger) else { + let Some(edge) = + node_and_trigger_to_edge(&self.graph, current_node, trigger, override_conditions) + else { return Ok(None); }; @@ -1892,8 +1909,13 @@ where } impl OpGraphPath { - fn next_edge_for_field(&self, field: &Field) -> Option { - self.graph.edge_for_field(self.tail, field) + fn next_edge_for_field( + &self, + field: &Field, + override_conditions: &EnabledOverrideConditions, + ) -> Option { + self.graph + .edge_for_field(self.tail, field, override_conditions) } fn next_edge_for_inline_fragment(&self, inline_fragment: &InlineFragment) -> Option { @@ -2027,6 +2049,7 @@ impl OpGraphPath { pub(crate) fn terminate_with_non_requested_typename_field( &self, + override_conditions: &EnabledOverrideConditions, ) -> Result { // If the last step of the path was a fragment/type-condition, we want to remove it before // we get __typename. The reason is that this avoid cases where this method would make us @@ -2066,7 +2089,10 @@ impl OpGraphPath { &tail_type_pos, None, ); - let Some(edge) = self.graph.edge_for_field(path.tail, &typename_field) else { + let Some(edge) = self + .graph + .edge_for_field(path.tail, &typename_field, override_conditions) + else { return Err(FederationError::internal( "Unexpectedly missing edge for __typename field", )); @@ -2401,6 +2427,7 @@ impl OpGraphPath { operation_element: &OpPathElement, context: &OpGraphPathContext, condition_resolver: &mut impl ConditionResolver, + override_conditions: &EnabledOverrideConditions, ) -> Result<(Option>, Option), FederationError> { let span = debug_span!("Trying to advance {self} directly with {operation_element}"); let _guard = span.enter(); @@ -2417,7 +2444,9 @@ impl OpGraphPath { OutputTypeDefinitionPosition::Object(tail_type_pos) => { // Just take the edge corresponding to the field, if it exists and can be // used. - let Some(edge) = self.next_edge_for_field(operation_field) else { + let Some(edge) = + self.next_edge_for_field(operation_field, override_conditions) + else { debug!( "No edge for field {operation_field} on object type {tail_weight}" ); @@ -2504,7 +2533,7 @@ impl OpGraphPath { let interface_edge = if field_is_of_an_implementation { None } else { - self.next_edge_for_field(operation_field) + self.next_edge_for_field(operation_field, override_conditions) }; let interface_path = if let Some(interface_edge) = &interface_edge { let field_path = self.add_field_edge( @@ -2668,6 +2697,7 @@ impl OpGraphPath { supergraph_schema.clone(), &implementation_inline_fragment.into(), condition_resolver, + override_conditions, )?; // If we find no options for that implementation, we bail (as we need to // simultaneously advance all implementations). @@ -2697,6 +2727,7 @@ impl OpGraphPath { supergraph_schema.clone(), operation_element, condition_resolver, + override_conditions, )?; let Some(field_options_for_implementation) = field_options_for_implementation @@ -2756,7 +2787,9 @@ impl OpGraphPath { } } OutputTypeDefinitionPosition::Union(_) => { - let Some(typename_edge) = self.next_edge_for_field(operation_field) else { + let Some(typename_edge) = + self.next_edge_for_field(operation_field, override_conditions) + else { return Err(FederationError::internal( "Should always have an edge for __typename edge on an union", )); @@ -2871,6 +2904,7 @@ impl OpGraphPath { supergraph_schema.clone(), &implementation_inline_fragment.into(), condition_resolver, + override_conditions, )?; let Some(implementation_options) = implementation_options else { drop(guard); @@ -3277,6 +3311,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { updated_context: &OpGraphPathContext, path_index: usize, condition_resolver: &mut impl ConditionResolver, + override_conditions: &EnabledOverrideConditions, ) -> Result { // Note that the provided context will usually be one we had during construction (the // `updated_context` will be `self.context` updated by whichever operation we're looking at, @@ -3284,12 +3319,13 @@ impl SimultaneousPathsWithLazyIndirectPaths { // rare), which is why we save recomputation by caching the computed value in that case, but // in case it's different, we compute without caching. if *updated_context != self.context { - self.compute_indirect_paths(path_index, condition_resolver)?; + self.compute_indirect_paths(path_index, condition_resolver, override_conditions)?; } if let Some(indirect_paths) = &self.lazily_computed_indirect_paths[path_index] { Ok(indirect_paths.clone()) } else { - let new_indirect_paths = self.compute_indirect_paths(path_index, condition_resolver)?; + let new_indirect_paths = + self.compute_indirect_paths(path_index, condition_resolver, override_conditions)?; self.lazily_computed_indirect_paths[path_index] = Some(new_indirect_paths.clone()); Ok(new_indirect_paths) } @@ -3299,17 +3335,21 @@ impl SimultaneousPathsWithLazyIndirectPaths { &self, path_index: usize, condition_resolver: &mut impl ConditionResolver, + overridden_conditions: &EnabledOverrideConditions, ) -> Result { self.paths.0[path_index].advance_with_non_collecting_and_type_preserving_transitions( &self.context, condition_resolver, &self.excluded_destinations, &self.excluded_conditions, + overridden_conditions, // The transitions taken by this method are non-collecting transitions, in which case // the trigger is the context (which is really a hack to provide context information for // keys during fetch dependency graph updating). |_, context| OpGraphPathTrigger::Context(context.clone()), - |graph, node, trigger| graph.edge_for_op_graph_path_trigger(node, trigger), + |graph, node, trigger, overridden_conditions| { + graph.edge_for_op_graph_path_trigger(node, trigger, overridden_conditions) + }, ) } @@ -3345,6 +3385,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { supergraph_schema: ValidFederationSchema, operation_element: &OpPathElement, condition_resolver: &mut impl ConditionResolver, + override_conditions: &EnabledOverrideConditions, ) -> Result>, FederationError> { debug!( "Trying to advance paths for operation: path = {}, operation = {operation_element}", @@ -3375,6 +3416,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { operation_element, &updated_context, condition_resolver, + override_conditions, )?; debug!("{advance_options:?}"); drop(gaurd); @@ -3422,7 +3464,12 @@ impl SimultaneousPathsWithLazyIndirectPaths { if let OpPathElement::Field(operation_field) = operation_element { // Add whatever options can be obtained by taking some non-collecting edges first. let paths_with_non_collecting_edges = self - .indirect_options(&updated_context, path_index, condition_resolver)? + .indirect_options( + &updated_context, + path_index, + condition_resolver, + override_conditions, + )? .filter_non_collecting_paths_for_field(operation_field)?; if !paths_with_non_collecting_edges.paths.is_empty() { debug!( @@ -3441,6 +3488,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { operation_element, &updated_context, condition_resolver, + override_conditions, )?; // If we can't advance the operation element after that path, ignore it, // it's just not an option. @@ -3528,6 +3576,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { operation_element, &updated_context, condition_resolver, + override_conditions, )?; options = advance_options.unwrap_or_else(Vec::new); debug!("{options:?}"); @@ -3552,11 +3601,6 @@ impl SimultaneousPathsWithLazyIndirectPaths { // PORT_NOTE: JS passes a ConditionResolver here, we do not: see port note for // `SimultaneousPathsWithLazyIndirectPaths` -// TODO(@goto-bus-stop): JS passes `override_conditions` here and maintains stores -// references to it in the created paths. AFAICT override conditions -// are shared mutable state among different query graphs, so having references to -// it in many structures would require synchronization. We should likely pass it as -// an argument to exactly the functionality that uses it. pub fn create_initial_options( initial_path: GraphPath>, initial_type: &QueryGraphNodeType, @@ -3564,6 +3608,7 @@ pub fn create_initial_options( condition_resolver: &mut impl ConditionResolver, excluded_edges: ExcludedDestinations, excluded_conditions: ExcludedConditions, + override_conditions: &EnabledOverrideConditions, ) -> Result, FederationError> { let initial_paths = SimultaneousPaths::from(initial_path); let mut lazy_initial_path = SimultaneousPathsWithLazyIndirectPaths::new( @@ -3574,8 +3619,12 @@ pub fn create_initial_options( ); if initial_type.is_federated_root_type() { - let initial_options = - lazy_initial_path.indirect_options(&initial_context, 0, condition_resolver)?; + let initial_options = lazy_initial_path.indirect_options( + &initial_context, + 0, + condition_resolver, + override_conditions, + )?; let options = initial_options .paths .iter() diff --git a/apollo-federation/src/query_graph/mod.rs b/apollo-federation/src/query_graph/mod.rs index f95588446c..4060ffdbe6 100644 --- a/apollo-federation/src/query_graph/mod.rs +++ b/apollo-federation/src/query_graph/mod.rs @@ -44,6 +44,7 @@ use crate::query_graph::graph_path::ExcludedDestinations; use crate::query_graph::graph_path::OpGraphPathContext; use crate::query_graph::graph_path::OpGraphPathTrigger; use crate::query_graph::graph_path::OpPathElement; +use crate::query_plan::query_planner::EnabledOverrideConditions; use crate::query_plan::QueryPlanCost; #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -147,6 +148,25 @@ pub(crate) struct QueryGraphEdge { /// /// Outside of keys, @requires edges also rely on conditions. pub(crate) conditions: Option>, + /// Edges can require that an override condition (provided during query + /// planning) be met in order to be taken. This is used for progressive + /// @override, where (at least) 2 subgraphs can resolve the same field, but + /// one of them has an @override with a label. If the override condition + /// matches the query plan parameters, this edge can be taken. + pub(crate) override_condition: Option, +} + +impl QueryGraphEdge { + fn satisfies_override_conditions( + &self, + conditions_to_check: &EnabledOverrideConditions, + ) -> bool { + if let Some(override_condition) = &self.override_condition { + override_condition.condition == conditions_to_check.contains(&override_condition.label) + } else { + true + } + } } impl Display for QueryGraphEdge { @@ -158,13 +178,32 @@ impl Display for QueryGraphEdge { { return Ok(()); } - if let Some(conditions) = &self.conditions { - write!(f, "{} ⊢ {}", conditions, self.transition) - } else { - self.transition.fmt(f) + + match (&self.override_condition, &self.conditions) { + (Some(override_condition), Some(conditions)) => write!( + f, + "{}, {} ⊢ {}", + conditions, override_condition, self.transition + ), + (Some(override_condition), None) => { + write!(f, "{} ⊢ {}", override_condition, self.transition) + } + (None, Some(conditions)) => write!(f, "{} ⊢ {}", conditions, self.transition), + _ => self.transition.fmt(f), } } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct OverrideCondition { + pub(crate) label: String, + pub(crate) condition: bool, +} + +impl Display for OverrideCondition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{} = {}", self.label, self.condition) + } +} /// The type of query graph edge "transition". /// @@ -639,7 +678,12 @@ impl QueryGraph { .find_ok(|selection| !external_metadata.selects_any_external_field(selection)) } - pub(crate) fn edge_for_field(&self, node: NodeIndex, field: &Field) -> Option { + pub(crate) fn edge_for_field( + &self, + node: NodeIndex, + field: &Field, + override_conditions: &EnabledOverrideConditions, + ) -> Option { let mut candidates = self.out_edges(node).into_iter().filter_map(|edge_ref| { let edge_weight = edge_ref.weight(); let QueryGraphEdgeTransition::FieldCollection { @@ -649,6 +693,11 @@ impl QueryGraph { else { return None; }; + + if !edge_weight.satisfies_override_conditions(override_conditions) { + return None; + } + // We explicitly avoid comparing parent type's here, to allow interface object // fields to match operation fields with the same name but differing types. if field.field_position.field_name() == field_definition_position.field_name() { @@ -715,12 +764,15 @@ impl QueryGraph { &self, node: NodeIndex, op_graph_path_trigger: &OpGraphPathTrigger, + override_conditions: &EnabledOverrideConditions, ) -> Option> { let OpGraphPathTrigger::OpPathElement(op_path_element) = op_graph_path_trigger else { return None; }; match op_path_element { - OpPathElement::Field(field) => self.edge_for_field(node, field).map(Some), + OpPathElement::Field(field) => self + .edge_for_field(node, field, override_conditions) + .map(Some), OpPathElement::InlineFragment(inline_fragment) => { if inline_fragment.type_condition_position.is_some() { self.edge_for_inline_fragment(node, inline_fragment) diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index c2c2d75805..a64f79364c 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -1,10 +1,11 @@ use std::cell::Cell; use std::num::NonZeroU32; +use std::ops::Deref; use std::sync::Arc; +use apollo_compiler::collections::HashSet; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; -use apollo_compiler::schema::ExtendedType; use apollo_compiler::validation::Valid; use apollo_compiler::ExecutableDocument; use apollo_compiler::Name; @@ -48,7 +49,6 @@ use crate::utils::logging::snapshot; use crate::ApiSchemaOptions; use crate::Supergraph; -pub(crate) const OVERRIDE_LABEL_ARG_NAME: &str = "overrideLabel"; pub(crate) const CONTEXT_DIRECTIVE: &str = "context"; pub(crate) const JOIN_FIELD: &str = "join__field"; @@ -196,6 +196,28 @@ impl QueryPlannerConfig { } } +#[derive(Debug, Default, Clone)] +pub struct QueryPlanOptions { + /// A set of labels which will be used _during query planning_ to + /// enable/disable edges with a matching label in their override condition. + /// Edges with override conditions require their label to be present or absent + /// from this set in order to be traversable. These labels enable the + /// progressive @override feature. + // PORT_NOTE: In JS implementation this was a Map + pub override_conditions: Vec, +} + +#[derive(Debug, Default, Clone)] +pub(crate) struct EnabledOverrideConditions(HashSet); + +impl Deref for EnabledOverrideConditions { + type Target = HashSet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + pub struct QueryPlanner { config: QueryPlannerConfig, federated_query_graph: Arc, @@ -308,11 +330,6 @@ impl QueryPlanner { .filter(|position| is_inconsistent(position.clone())) .collect::>(); - // PORT_NOTE: JS prepares a map of override conditions here, which is - // a map where the keys are all `@join__field(overrideLabel:)` argument values - // and the values are all initialised to `false`. Instead of doing that, we should - // be able to use a Set where presence means `true` and absence means `false`. - Ok(Self { config, federated_query_graph: Arc::new(query_graph), @@ -339,6 +356,7 @@ impl QueryPlanner { &self, document: &Valid, operation_name: Option, + options: QueryPlanOptions, ) -> Result { let operation = document .operations @@ -482,7 +500,9 @@ impl QueryPlanner { .clone() .into(), config: self.config.clone(), - // PORT_NOTE: JS provides `override_conditions` here: see port note in `QueryPlanner::new`. + override_conditions: EnabledOverrideConditions(HashSet::from_iter( + options.override_conditions, + )), }; let root_node = match defer_conditions { @@ -549,59 +569,6 @@ impl QueryPlanner { } fn check_unsupported_features(supergraph: &Supergraph) -> Result<(), FederationError> { - // We have a *progressive* override when `join__field` has a - // non-null value for `overrideLabel` field. - // - // This looks at object types' fields and their directive - // applications, looking specifically for `@join__field` - // arguments list. - let has_progressive_overrides = supergraph - .schema - .schema() - .types - .values() - .filter_map(|extended_type| { - // The override label args can be only on ObjectTypes - if let ExtendedType::Object(object_type) = extended_type { - Some(object_type) - } else { - None - } - }) - .flat_map(|object_type| &object_type.fields) - .flat_map(|(_, field)| { - field - .directives - .iter() - .filter(|d| d.name.as_str() == JOIN_FIELD) - }) - .any(|join_directive| { - if let Some(override_label_arg) = - join_directive.argument_by_name(OVERRIDE_LABEL_ARG_NAME) - { - // Any argument value for `overrideLabel` that's not - // null can be considered as progressive override usage - if !override_label_arg.is_null() { - return true; - } - return false; - } - false - }); - if has_progressive_overrides { - let message = "\ - `experimental_query_planner_mode: new` or `both` cannot yet \ - be used with progressive overrides. \ - Remove uses of progressive overrides to try the experimental query planner, \ - otherwise switch back to `legacy` or `both_best_effort`.\ - "; - return Err(SingleFederationError::UnsupportedFeature { - message: message.to_owned(), - kind: crate::error::UnsupportedFeatureKind::ProgressiveOverrides, - } - .into()); - } - // We will only check for `@context` direcive, since // `@fromContext` can only be used if `@context` is already // applied, and we assume a correctly composed supergraph. @@ -1102,7 +1069,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "accounts") { @@ -1134,7 +1103,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Sequence { @@ -1223,7 +1194,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Parallel { @@ -1334,7 +1307,9 @@ type User let mut config = QueryPlannerConfig::default(); config.debug.bypass_planner_for_single_subgraph = true; let planner = QueryPlanner::new(&supergraph, config).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "A") { @@ -1378,7 +1353,9 @@ type User .unwrap(); let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "accounts") { @@ -1437,7 +1414,9 @@ type User .unwrap(); let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "accounts") { @@ -1497,7 +1476,9 @@ type User .unwrap(); let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); // Make sure `fragment F2` contains `...F1`. insta::assert_snapshot!(plan, @r###" QueryPlan { @@ -1554,7 +1535,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "Subgraph1") { diff --git a/apollo-federation/src/query_plan/query_planning_traversal.rs b/apollo-federation/src/query_plan/query_planning_traversal.rs index c54a81ba66..cb5e6bb9f1 100644 --- a/apollo-federation/src/query_plan/query_planning_traversal.rs +++ b/apollo-federation/src/query_plan/query_planning_traversal.rs @@ -36,6 +36,7 @@ use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphToC use crate::query_plan::generate::generate_all_plans_and_find_best; use crate::query_plan::generate::PlanBuilder; use crate::query_plan::query_planner::compute_root_fetch_groups; +use crate::query_plan::query_planner::EnabledOverrideConditions; use crate::query_plan::query_planner::QueryPlannerConfig; use crate::query_plan::query_planner::QueryPlanningStatistics; use crate::query_plan::QueryPlanCost; @@ -72,6 +73,7 @@ pub(crate) struct QueryPlanningParameters<'a> { /// The configuration for the query planner. pub(crate) config: QueryPlannerConfig, pub(crate) statistics: &'a QueryPlanningStatistics, + pub(crate) override_conditions: EnabledOverrideConditions, } pub(crate) struct QueryPlanningTraversal<'a, 'b> { @@ -247,6 +249,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { &mut traversal, excluded_destinations, excluded_conditions, + ¶meters.override_conditions, )?; traversal.open_branches = map_options_to_selections(selection_set, initial_options); @@ -337,6 +340,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { self.parameters.supergraph_schema.clone(), &operation_element, /*resolver*/ self, + &self.parameters.override_conditions, )?; let Some(followups_for_option) = followups_for_option else { // There is no valid way to advance the current operation element from this option @@ -404,7 +408,9 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { let mut new_simultaneous_paths = vec![]; for simultaneous_path in &option.paths.0 { new_simultaneous_paths.push(Arc::new( - simultaneous_path.terminate_with_non_requested_typename_field()?, + simultaneous_path.terminate_with_non_requested_typename_field( + &self.parameters.override_conditions, + )?, )); } closed_paths.push(Arc::new(ClosedPath { @@ -1054,6 +1060,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { .clone(), config: self.parameters.config.clone(), statistics: self.parameters.statistics, + override_conditions: self.parameters.override_conditions.clone(), }; let best_plan_opt = QueryPlanningTraversal::new_inner( ¶meters, diff --git a/apollo-federation/tests/query_plan/build_query_plan_support.rs b/apollo-federation/tests/query_plan/build_query_plan_support.rs index f73c3117f7..b73596590b 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_support.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_support.rs @@ -59,6 +59,18 @@ macro_rules! subgraph_name { /// formatted query plan string. /// Run `cargo insta review` to diff and accept changes to the generated query plan. macro_rules! assert_plan { + ($api_schema_and_planner: expr, $operation: expr, $options: expr, @$expected: literal) => {{ + let (api_schema, planner) = $api_schema_and_planner; + let document = apollo_compiler::ExecutableDocument::parse_and_validate( + api_schema.schema(), + $operation, + "operation.graphql", + ) + .unwrap(); + let plan = planner.build_query_plan(&document, None, $options).unwrap(); + insta::assert_snapshot!(plan, @$expected); + plan + }}; ($api_schema_and_planner: expr, $operation: expr, @$expected: literal) => {{ let (api_schema, planner) = $api_schema_and_planner; let document = apollo_compiler::ExecutableDocument::parse_and_validate( @@ -67,7 +79,7 @@ macro_rules! assert_plan { "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner.build_query_plan(&document, None, Default::default()).unwrap(); insta::assert_snapshot!(plan, @$expected); plan }}; 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 0d618a6d0f..acef092c2c 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests.rs @@ -44,11 +44,11 @@ mod merged_abstract_types_handling; mod mutations; mod named_fragments; mod named_fragments_preservation; +mod overrides; mod provides; mod requires; mod shareable_root_fields; mod subscriptions; - // TODO: port the rest of query-planner-js/src/__tests__/buildPlan.test.ts #[test] diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs index a18565aed0..884c10e7e1 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs @@ -253,7 +253,9 @@ fn correctly_handle_case_where_there_is_too_many_plans_to_consider() { "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); // Note: The way the code that handle multiple plans currently work, it mess up the order of fields a bit. It's not a // big deal in practice cause everything gets re-order in practice during actual execution, but this means it's a tad diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/overrides.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides.rs new file mode 100644 index 0000000000..c0f1b91a31 --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides.rs @@ -0,0 +1,375 @@ +use apollo_federation::query_plan::query_planner::QueryPlanOptions; + +mod shareable; + +#[test] +fn it_handles_progressive_override_on_root_fields() { + let planner = planner!( + s1: r#" + type Query { + hello: String + } + "#, + s2: r#" + type Query { + hello: String @override(from: "s1", label: "test") + } + "#, + ); + assert_plan!( + &planner, + r#" + { + hello + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Fetch(service: "s2") { + { + hello + } + }, + } + "### + ); +} + +#[test] +fn it_does_not_override_unset_labels_on_root_fields() { + let planner = planner!( + s1: r#" + type Query { + hello: String + } + "#, + s2: r#" + type Query { + hello: String @override(from: "s1", label: "test") + } + "#, + ); + assert_plan!( + &planner, + r#" + { + hello + } + "#, + + @r###" + QueryPlan { + Fetch(service: "s1") { + { + hello + } + }, + } + "### + ); +} + +#[test] +fn it_handles_progressive_override_on_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "s2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f1 + f2 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_does_not_override_unset_labels_on_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + + @r###" + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + t { + __typename + id + f1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "s2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f2 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_handles_progressive_override_on_nested_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t2 { + t { + f1 + } + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + t2 { + t { + __typename + id + } + } + } + }, + Flatten(path: "t2.t") { + Fetch(service: "s2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f1 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_does_not_override_unset_labels_on_nested_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t2 { + t { + f1 + } + } + } + "#, + + @r###" + QueryPlan { + Fetch(service: "s1") { + { + t2 { + t { + f1 + } + } + } + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/overrides/shareable.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides/shareable.rs new file mode 100644 index 0000000000..103017912e --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides/shareable.rs @@ -0,0 +1,244 @@ +use apollo_federation::query_plan::query_planner::QueryPlanOptions; + +const S1: &str = r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + f1: String @shareable + } +"#; + +const S2: &str = r#" + type T @key(fields: "id") { + id: ID! + f1: String @shareable @override(from: "S1", label: "test") + f2: String + } +"#; + +const S3: &str = r#" + type T @key(fields: "id") { + id: ID! + f1: String @shareable + f3: String + } +"#; + +#[test] +fn it_overrides_to_s2_when_label_is_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "S2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f2 + f1 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_resolves_in_s1_when_label_is_not_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + f1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "S2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f2 + } + } + }, + }, + }, + } + "### + ); +} + +// This is very similar to the S2 example. The fact that the @override in S2 +// specifies _from_ S1 actually affects all T.f1 fields the same way (except +// S1). That is to say, it's functionally equivalent to have the `@override` +// exist in either S2 or S3 from S2/S3/Sn's perspective. It's helpful to +// test here that the QP will take a path through _either_ S2 or S3 when +// appropriate to do so. In these tests and the previous S2 tests, +// "appropriate" is determined by the other fields being selected in the +// query. +#[test] +fn it_overrides_f1_to_s3_when_label_is_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f3 + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "S3") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f3 + f1 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_resolves_f1_in_s1_when_label_is_not_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f3 + } + } + "#, + + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + f1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "S3") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f3 + } + } + }, + }, + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql new file mode 100644 index 0000000000..206487ca49 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql @@ -0,0 +1,53 @@ +# Composed from subgraphs with hash: b409284c35003f62c7c83675734478b1970effb2 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + hello: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql new file mode 100644 index 0000000000..206487ca49 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql @@ -0,0 +1,53 @@ +# Composed from subgraphs with hash: b409284c35003f62c7c83675734478b1970effb2 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + hello: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs index 7cf8b1ba4a..752479b256 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs @@ -725,7 +725,9 @@ mod tests { let planner = QueryPlanner::new(schema.federation_supergraph(), Default::default()).unwrap(); - let query_plan = planner.build_query_plan(&query.executable, None).unwrap(); + let query_plan = planner + .build_query_plan(&query.executable, None, Default::default()) + .unwrap(); let schema = DemandControlledSchema::new(Arc::new(schema.supergraph_schema().clone())).unwrap(); diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 24ad85ee17..9cab43aab8 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -12,6 +12,7 @@ use apollo_compiler::validation::Valid; use apollo_compiler::Name; use apollo_federation::error::FederationError; use apollo_federation::error::SingleFederationError; +use apollo_federation::query_plan::query_planner::QueryPlanOptions; use apollo_federation::query_plan::query_planner::QueryPlanner; use futures::future::BoxFuture; use opentelemetry_api::metrics::MeterProvider as _; @@ -68,7 +69,6 @@ use crate::Configuration; pub(crate) const RUST_QP_MODE: &str = "rust"; pub(crate) const JS_QP_MODE: &str = "js"; const UNSUPPORTED_CONTEXT: &str = "context"; -const UNSUPPORTED_OVERRIDES: &str = "overrides"; const UNSUPPORTED_FED1: &str = "fed1"; const INTERNAL_INIT_ERROR: &str = "internal"; @@ -203,9 +203,6 @@ impl PlannerMode { metric_rust_qp_init(Some(UNSUPPORTED_FED1)); } SingleFederationError::UnsupportedFeature { message: _, kind } => match kind { - apollo_federation::error::UnsupportedFeatureKind::ProgressiveOverrides => { - metric_rust_qp_init(Some(UNSUPPORTED_OVERRIDES)) - } apollo_federation::error::UnsupportedFeatureKind::Context => { metric_rust_qp_init(Some(UNSUPPORTED_CONTEXT)) } @@ -298,12 +295,20 @@ impl PlannerMode { let (plan, mut root_node) = tokio::task::spawn_blocking(move || { let start = Instant::now(); + let query_plan_options = QueryPlanOptions { + override_conditions: plan_options.override_conditions, + }; + let result = operation .as_deref() .map(|n| Name::new(n).map_err(FederationError::from)) .transpose() .and_then(|operation| { - rust_planner.build_query_plan(&doc.executable, operation) + rust_planner.build_query_plan( + &doc.executable, + operation, + query_plan_options, + ) }) .map_err(|e| QueryPlannerError::FederationError(e.to_string())); @@ -346,7 +351,7 @@ impl PlannerMode { let start = Instant::now(); let result = js - .plan(filtered_query, operation.clone(), plan_options) + .plan(filtered_query, operation.clone(), plan_options.clone()) .await; let elapsed = start.elapsed().as_secs_f64(); @@ -365,6 +370,9 @@ impl PlannerMode { } } + let query_plan_options = QueryPlanOptions { + override_conditions: plan_options.override_conditions, + }; BothModeComparisonJob { rust_planner: rust.clone(), js_duration: elapsed, @@ -375,6 +383,7 @@ impl PlannerMode { .as_ref() .map(|success| success.data.clone()) .map_err(|e| e.errors.clone()), + plan_options: query_plan_options, } .schedule(); @@ -941,9 +950,9 @@ impl BridgeQueryPlanner { if has_schema_introspection { if has_other_root_fields { let error = graphql::Error::builder() - .message("Mixed queries with both schema introspection and concrete fields are not supported") - .extension_code("MIXED_INTROSPECTION") - .build(); + .message("Mixed queries with both schema introspection and concrete fields are not supported") + .extension_code("MIXED_INTROSPECTION") + .build(); return Ok(QueryPlannerContent::Response { response: Box::new(graphql::Response::builder().error(error).build()), }); @@ -1242,8 +1251,8 @@ mod tests { &doc, query_metrics ) - .await - .unwrap_err(); + .await + .unwrap_err(); match err { QueryPlannerError::EmptyPlan(usage_reporting) => { @@ -1347,7 +1356,7 @@ mod tests { } }}"#); // Aliases - // FIXME: uncomment myName alias when this is fixed: + // FIXME: uncomment myName alias when this is fixed: // https://github.com/apollographql/router/issues/3263 s!(r#"query Q { me { username @@ -1797,13 +1806,6 @@ mod tests { "init.error_kind" = "context", "init.is_success" = false ); - metric_rust_qp_init(Some(UNSUPPORTED_OVERRIDES)); - assert_counter!( - "apollo.router.lifecycle.query_planner.init", - 1, - "init.error_kind" = "overrides", - "init.is_success" = false - ); metric_rust_qp_init(Some(UNSUPPORTED_FED1)); assert_counter!( "apollo.router.lifecycle.query_planner.init", diff --git a/apollo-router/src/query_planner/dual_query_planner.rs b/apollo-router/src/query_planner/dual_query_planner.rs index e368582a97..9952eb9782 100644 --- a/apollo-router/src/query_planner/dual_query_planner.rs +++ b/apollo-router/src/query_planner/dual_query_planner.rs @@ -12,6 +12,7 @@ use apollo_compiler::ast; use apollo_compiler::validation::Valid; use apollo_compiler::ExecutableDocument; use apollo_compiler::Name; +use apollo_federation::query_plan::query_planner::QueryPlanOptions; use apollo_federation::query_plan::query_planner::QueryPlanner; use apollo_federation::query_plan::QueryPlan; @@ -43,6 +44,7 @@ pub(crate) struct BothModeComparisonJob { pub(crate) document: Arc>, pub(crate) operation_name: Option, pub(crate) js_result: Result>>, + pub(crate) plan_options: QueryPlanOptions, } type Queue = crossbeam_channel::Sender; @@ -88,7 +90,9 @@ impl BothModeComparisonJob { let start = Instant::now(); // No question mark operator or macro from here … - let result = self.rust_planner.build_query_plan(&self.document, name); + let result = + self.rust_planner + .build_query_plan(&self.document, name, self.plan_options); let elapsed = start.elapsed().as_secs_f64(); metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); diff --git a/apollo-router/tests/integration/query_planner.rs b/apollo-router/tests/integration/query_planner.rs index 88ace09147..03056ab47d 100644 --- a/apollo-router/tests/integration/query_planner.rs +++ b/apollo-router/tests/integration/query_planner.rs @@ -158,112 +158,6 @@ async fn fed2_schema_with_new_qp() { router.graceful_shutdown().await; } -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_legacy_qp() { - if !graph_os_enabled() { - return; - } - let mut router = IntegrationTest::builder() - .config(LEGACY_QP) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_new_qp() { - if !graph_os_enabled() { - return; - } - let mut router = IntegrationTest::builder() - .config(NEW_QP) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router - .assert_log_contains( - "could not create router: \ - failed to initialize the query planner: \ - `experimental_query_planner_mode: new` or `both` cannot yet \ - be used with progressive overrides. \ - Remove uses of progressive overrides to try the experimental query planner, \ - otherwise switch back to `legacy` or `both_best_effort`.", - ) - .await; - router.assert_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_legacy_qp_change_to_new_qp_keeps_old_config() { - if !graph_os_enabled() { - return; - } - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); - let mut router = IntegrationTest::builder() - .config(config) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{NEW_QP}"); - router.update_config(&config).await; - router - .assert_log_contains("error while reloading, continuing with previous configuration") - .await; - router - .assert_metrics_contains( - r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="overrides",init_is_success="false",otel_scope_name="apollo/router"} 1"#, - None, - ) - .await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_legacy_qp_reload_to_both_best_effort_keep_previous_config() { - if !graph_os_enabled() { - return; - } - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); - let mut router = IntegrationTest::builder() - .config(config) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{BOTH_BEST_EFFORT_QP}"); - router.update_config(&config).await; - router - .assert_log_contains( - "Falling back to the legacy query planner: \ - failed to initialize the query planner: \ - `experimental_query_planner_mode: new` or `both` cannot yet \ - be used with progressive overrides. \ - Remove uses of progressive overrides to try the experimental query planner, \ - otherwise switch back to `legacy` or `both_best_effort`.", - ) - .await; - router - .assert_metrics_contains( - r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="overrides",init_is_success="false",otel_scope_name="apollo/router"} 1"#, - None, - ) - .await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - #[tokio::test(flavor = "multi_thread")] async fn context_with_legacy_qp() { if !graph_os_enabled() { From 94d7332ed55429afa362950f4d0909074d6b8f89 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Thu, 19 Sep 2024 14:14:04 +0200 Subject: [PATCH 34/56] Follow up fixes for authorization filtering (#6025) --- ...transform__tests__remove_directive-10.snap | 29 +++ ...transform__tests__remove_directive-11.snap | 23 +++ ...transform__tests__remove_directive-12.snap | 17 ++ ...transform__tests__remove_directive-13.snap | 17 ++ ...transform__tests__remove_directive-14.snap | 17 ++ ..._transform__tests__remove_directive-7.snap | 17 ++ ..._transform__tests__remove_directive-8.snap | 17 ++ ..._transform__tests__remove_directive-9.snap | 29 +++ apollo-router/src/spec/query/transform.rs | 194 +++++++++++++++--- 9 files changed, 336 insertions(+), 24 deletions(-) create mode 100644 apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-10.snap create mode 100644 apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-11.snap create mode 100644 apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-12.snap create mode 100644 apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-13.snap create mode 100644 apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-14.snap create mode 100644 apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-7.snap create mode 100644 apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-8.snap create mode 100644 apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-9.snap diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-10.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-10.snap new file mode 100644 index 0000000000..b6f6e5c2d4 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-10.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query Test($a: String, $b: String) { + ...TestFragment @hasArg(arg: $a) + ...TestFragment2 @hasArg(arg: $b) + c + } + + fragment TestFragment on Query { + __typename @remove + } + + fragment TestFragment2 on Query { + __typename + } + +filtered: +query Test($b: String) { + ...TestFragment2 @hasArg(arg: $b) + c +} + +fragment TestFragment2 on Query { + __typename +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-11.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-11.snap new file mode 100644 index 0000000000..3f17858f2e --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-11.snap @@ -0,0 +1,23 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query Test($a: String, $b: String) { + ... @hasArg(arg: $a) { + c @remove + } + ... @hasArg(arg: $b) { + test: c + } + c + } + +filtered: +query Test($b: String) { + ... @hasArg(arg: $b) { + test: c + } + c +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-12.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-12.snap new file mode 100644 index 0000000000..58212cd6b3 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-12.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String, $b: String) { + c + f(arg: [["a"], [$a], ["b"]]) @remove + aliased: f(arg: [["a"], [$b]]) + } + +filtered: +query($b: String) { + c + aliased: f(arg: [["a"], [$b]]) +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-13.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-13.snap new file mode 100644 index 0000000000..17670d39e6 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-13.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String, $b: String) { + c + g(arg: [{a: $a}, {a: "a"}]) @remove + aliased: g(arg: [{a: "a"}, {a: $b}]) + } + +filtered: +query($b: String) { + c + aliased: g(arg: [{a: "a"}, {a: $b}]) +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-14.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-14.snap new file mode 100644 index 0000000000..938fea77c7 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-14.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String, $b: String) { + c + e(arg: {c: [$a]}) @remove + aliased: e(arg: {c: [$b]}) + } + +filtered: +query($b: String) { + c + aliased: e(arg: {c: [$b]}) +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-7.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-7.snap new file mode 100644 index 0000000000..f0576d4b3d --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-7.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String, $b: String) { + c + d(arg: ["a", $a, "b"]) @remove + aliased: d(arg: [$b]) + } + +filtered: +query($b: String) { + c + aliased: d(arg: [$b]) +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-8.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-8.snap new file mode 100644 index 0000000000..dbe3c6cf62 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-8.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String, $b: String) { + c + e(arg: {a: $a, b: "b"}) @remove + aliased: e(arg: {a: "a", b: $b}) + } + +filtered: +query($b: String) { + c + aliased: e(arg: {a: "a", b: $b}) +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-9.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-9.snap new file mode 100644 index 0000000000..9b149e9db6 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-9.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query Test($a: String, $b: String, $c: String) @hasArg(arg: $a) { + ...TestFragment + ...TestFragment2 + c + } + + fragment TestFragment on Query @hasArg(arg: $b) { + __typename @remove + } + + fragment TestFragment2 on Query @hasArg(arg: $c) { + __typename + } + +filtered: +query Test($a: String, $c: String) @hasArg(arg: $a) { + ...TestFragment2 + c +} + +fragment TestFragment2 on Query @hasArg(arg: $c) { + __typename +} diff --git a/apollo-router/src/spec/query/transform.rs b/apollo-router/src/spec/query/transform.rs index 078634f1d6..8b31d6054b 100644 --- a/apollo-router/src/spec/query/transform.rs +++ b/apollo-router/src/spec/query/transform.rs @@ -251,6 +251,12 @@ pub(crate) fn operation( return Ok(None); }; + for directive in def.directives.iter() { + for argument in directive.arguments.iter() { + used_variables_from_value(visitor, &argument.value); + } + } + Ok(Some(ast::OperationDefinition { name: def.name.clone(), operation_type: def.operation_type, @@ -271,6 +277,13 @@ pub(crate) fn fragment_definition( else { return Ok(None); }; + + for directive in def.directives.iter() { + for argument in directive.arguments.iter() { + used_variables_from_value(visitor, &argument.value); + } + } + Ok(Some(ast::FragmentDefinition { name: def.name.clone(), type_condition: def.type_condition.clone(), @@ -294,22 +307,12 @@ pub(crate) fn field( }; for argument in def.arguments.iter() { - if let Some(var) = argument.value.as_variable() { - visitor - .state() - .used_variables - .insert(var.as_str().to_string()); - } + used_variables_from_value(visitor, &argument.value); } for directive in def.directives.iter() { for argument in directive.arguments.iter() { - if let Some(var) = argument.value.as_variable() { - visitor - .state() - .used_variables - .insert(var.as_str().to_string()); - } + used_variables_from_value(visitor, &argument.value); } } @@ -336,12 +339,7 @@ pub(crate) fn fragment_spread( for directive in def.directives.iter() { for argument in directive.arguments.iter() { - if let Some(var) = argument.value.as_variable() { - visitor - .state() - .used_variables - .insert(var.as_str().to_string()); - } + used_variables_from_value(visitor, &argument.value); } } @@ -362,12 +360,7 @@ pub(crate) fn inline_fragment( for directive in def.directives.iter() { for argument in directive.arguments.iter() { - if let Some(var) = argument.value.as_variable() { - visitor - .state() - .used_variables - .insert(var.as_str().to_string()); - } + used_variables_from_value(visitor, &argument.value); } } @@ -424,6 +417,31 @@ pub(crate) fn selection_set( Ok((!selections.is_empty()).then_some(selections)) } +fn used_variables_from_value( + visitor: &mut V, + argument_value: &apollo_compiler::ast::Value, +) { + match argument_value { + apollo_compiler::ast::Value::Variable(name) => { + visitor + .state() + .used_variables + .insert(name.as_str().to_string()); + } + apollo_compiler::ast::Value::List(values) => { + for value in values { + used_variables_from_value(visitor, value); + } + } + apollo_compiler::ast::Value::Object(values) => { + for (_, value) in values { + used_variables_from_value(visitor, value); + } + } + _ => {} + } +} + /// this visitor goes through the list of fragments in the query, looking at fragment spreads /// in their selection, and generates a list of fragments in the order they should be visited /// by the transform visitor, to ensure a fragment has already been visited before it is @@ -705,6 +723,7 @@ fragment F on Query { } directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA directive @remove on FIELD | INLINE_FRAGMENT | FRAGMENT_SPREAD + directive @hasArg(arg: String!) on QUERY | FRAGMENT_DEFINITION | INLINE_FRAGMENT | FRAGMENT_SPREAD scalar link__Import enum link__Purpose { """ @@ -722,6 +741,17 @@ fragment F on Query { a(arg: String): String b: Obj c: Int + d(arg: [String]): String + e(arg: Inp): String + f(arg: [[String]]): String + g(arg: [Inp]): String + + } + + input Inp { + a: String + b: String + c: [String] } type Obj { @@ -827,5 +857,121 @@ fragment F on Query { let doc = ast::Document::parse(query, "query.graphql").unwrap(); let result = document(&mut visitor, &doc).unwrap(); insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in list argument + let query = r#" + query($a: String, $b: String) { + c + d(arg: ["a", $a, "b"]) @remove + aliased: d(arg: [$b]) + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in input argument + let query = r#" + query($a: String, $b: String) { + c + e(arg: {a: $a, b: "b"}) @remove + aliased: e(arg: {a: "a", b: $b}) + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in directive on operation and fragment + let query = r#" + query Test($a: String, $b: String, $c: String) @hasArg(arg: $a) { + ...TestFragment + ...TestFragment2 + c + } + + fragment TestFragment on Query @hasArg(arg: $b) { + __typename @remove + } + + fragment TestFragment2 on Query @hasArg(arg: $c) { + __typename + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in directive on fragment spread + let query = r#" + query Test($a: String, $b: String) { + ...TestFragment @hasArg(arg: $a) + ...TestFragment2 @hasArg(arg: $b) + c + } + + fragment TestFragment on Query { + __typename @remove + } + + fragment TestFragment2 on Query { + __typename + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in directive on inline fragment + let query = r#" + query Test($a: String, $b: String) { + ... @hasArg(arg: $a) { + c @remove + } + ... @hasArg(arg: $b) { + test: c + } + c + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in nested list argument + let query = r#" + query($a: String, $b: String) { + c + f(arg: [["a"], [$a], ["b"]]) @remove + aliased: f(arg: [["a"], [$b]]) + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in input type in list argument + let query = r#" + query($a: String, $b: String) { + c + g(arg: [{a: $a}, {a: "a"}]) @remove + aliased: g(arg: [{a: "a"}, {a: $b}]) + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in list in input type argument + let query = r#" + query($a: String, $b: String) { + c + e(arg: {c: [$a]}) @remove + aliased: e(arg: {c: [$b]}) + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); } } From 941866363c1e612415e38b4866cd6f60540f3127 Mon Sep 17 00:00:00 2001 From: Iryna Shestak Date: Thu, 19 Sep 2024 14:56:30 +0200 Subject: [PATCH 35/56] update router-bridge@0.6.2+v2.9.1 (#6029) Updates to latest router-bridge and federation version. This federation version fixes edge cases for subgraph extraction logic when using spec renaming or specs URLs that look similar to `specs.apollo.dev` --- .changesets/chore_update_router_bridge.md | 5 +++++ Cargo.lock | 4 ++-- apollo-router/Cargo.toml | 8 ++++---- apollo-router/tests/integration/redis.rs | 14 +++++++------- fuzz/Cargo.toml | 2 +- 5 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 .changesets/chore_update_router_bridge.md diff --git a/.changesets/chore_update_router_bridge.md b/.changesets/chore_update_router_bridge.md new file mode 100644 index 0000000000..0505eb1889 --- /dev/null +++ b/.changesets/chore_update_router_bridge.md @@ -0,0 +1,5 @@ +### Update router-bridge@0.6.2+v2.9.1 ([PR #6027](https://github.com/apollographql/router/pull/6027)) + +Updates to latest router-bridge and federation version. This federation version fixes edge cases for subgraph extraction logic when using spec renaming or specs URLs that look similar to `specs.apollo.dev` + +By [@lrlna](https://github.com/lrlna) in https://github.com/apollographql/router/pull/6027 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 385b8a5a5c..b3a1b1819f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5623,9 +5623,9 @@ dependencies = [ [[package]] name = "router-bridge" -version = "0.6.1+v2.9.0" +version = "0.6.2+v2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3be2d3ebbcbceb19dc813f5cee507295c673bb4e3f7a7cd532c8c27c95f92fc" +checksum = "a82c217157e756750386a5371da31590c89c315d953ae6d299d73949138e332f" dependencies = [ "anyhow", "async-channel 1.9.0", diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 51b6bd93ca..37c0812ab1 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -197,7 +197,7 @@ regex = "1.10.5" reqwest.workspace = true # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.6.1+v2.9.0" +router-bridge = "=0.6.2+v2.9.1" rust-embed = { version = "8.4.0", features = ["include-exclude"] } rustls = "0.21.12" @@ -268,9 +268,9 @@ aws-credential-types = "1.1.6" aws-config = "1.1.6" aws-types = "1.1.6" aws-smithy-runtime-api = { version = "1.1.6", features = ["client"] } -aws-sdk-sso = "=1.39.0" # TODO: unpin when on Rust 1.78+ -aws-sdk-ssooidc = "=1.40.0" # TODO: unpin when on Rust 1.78+ -aws-sdk-sts = "=1.39.0" # TODO: unpin when on Rust 1.78+ +aws-sdk-sso = "=1.39.0" # TODO: unpin when on Rust 1.78+ +aws-sdk-ssooidc = "=1.40.0" # TODO: unpin when on Rust 1.78+ +aws-sdk-sts = "=1.39.0" # TODO: unpin when on Rust 1.78+ sha1.workspace = true tracing-serde = "0.1.3" time = { version = "0.3.36", features = ["serde"] } diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index 30b71d46b3..cbd1f5dc0a 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -48,7 +48,7 @@ use crate::integration::IntegrationTest; async fn query_planner_cache() -> Result<(), BoxError> { // If this test fails and the cache key format changed you'll need to update the key here. // Look at the top of the file for instructions on getting the new cache key. - let known_cache_key = "plan:0:v2.9.0:70f115ebba5991355c17f4f56ba25bb093c519c4db49a30f3b10de279a4e3fa4:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:4f9f0183101b2f249a364b98adadfda6e5e2001d1f2465c988428cf1ac0b545f"; + let known_cache_key = "plan:0:v2.9.1:70f115ebba5991355c17f4f56ba25bb093c519c4db49a30f3b10de279a4e3fa4:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:4f9f0183101b2f249a364b98adadfda6e5e2001d1f2465c988428cf1ac0b545f"; let config = RedisConfig::from_url("redis://127.0.0.1:6379").unwrap(); let client = RedisClient::new(config, None, None, None); @@ -944,7 +944,7 @@ async fn connection_failure_blocks_startup() { async fn query_planner_redis_update_query_fragments() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_query_fragments.router.yaml"), - "plan:0:v2.9.0:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:a52c81e3e2e47c8363fbcd2653e196431c15716acc51fce4f58d9368ac4c2d8d", + "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:78f3ccab3def369f4b809a0f8c8f6e90545eb08cd1efeb188ffc663b902c1f2d", ) .await; } @@ -974,7 +974,7 @@ async fn query_planner_redis_update_introspection() { // test just passes locally. test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_introspection.router.yaml"), - "plan:0:v2.9.0:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:99a70d6c967eea3bc68721e1094f586f5ae53c7e12f83a650abd5758c372d048", + "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:99a70d6c967eea3bc68721e1094f586f5ae53c7e12f83a650abd5758c372d048", ) .await; } @@ -994,7 +994,7 @@ async fn query_planner_redis_update_defer() { // test just passes locally. test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), - "plan:0:v2.9.0:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:d6a3d7807bb94cfb26be4daeb35e974680b53755658fafd4c921c70cec1b7c39", + "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:d6a3d7807bb94cfb26be4daeb35e974680b53755658fafd4c921c70cec1b7c39", ) .await; } @@ -1016,7 +1016,7 @@ async fn query_planner_redis_update_type_conditional_fetching() { include_str!( "fixtures/query_planner_redis_config_update_type_conditional_fetching.router.yaml" ), - "plan:0:v2.9.0:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:8991411cc7b66d9f62ab1e661f2ce9ccaf53b0d203a275e43ced9a8b6bba02dd", + "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:8991411cc7b66d9f62ab1e661f2ce9ccaf53b0d203a275e43ced9a8b6bba02dd", ) .await; } @@ -1038,7 +1038,7 @@ async fn query_planner_redis_update_reuse_query_fragments() { include_str!( "fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml" ), - "plan:0:v2.9.0:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:c05e89caeb8efc4e8233e8648099b33414716fe901e714416fd0f65a67867f07", + "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:c05e89caeb8efc4e8233e8648099b33414716fe901e714416fd0f65a67867f07", ) .await; } @@ -1063,7 +1063,7 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key router.clear_redis_cache().await; // If the tests above are failing, this is the key that needs to be changed first. - let starting_key = "plan:0:v2.9.0:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:a52c81e3e2e47c8363fbcd2653e196431c15716acc51fce4f58d9368ac4c2d8d"; + let starting_key = "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:a52c81e3e2e47c8363fbcd2653e196431c15716acc51fce4f58d9368ac4c2d8d"; router.execute_default_query().await; router.assert_redis_cache_contains(starting_key, None).await; diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index f58f7256a9..38603f704b 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -20,7 +20,7 @@ reqwest = { workspace = true, features = ["json", "blocking"] } serde_json.workspace = true tokio.workspace = true # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.6.1+v2.9.0" +router-bridge = "=0.6.2+v2.9.1" [dev-dependencies] anyhow = "1" From 78aef22d4b3bb63abe0ee17100296b8e4010dd2b Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:36:24 -0500 Subject: [PATCH 36/56] feat(federation): add support for progressive overrides (#6011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement support for progressive `@override`s (https://github.com/apollographql/federation/pull/2902). Co-authored-by: Renée --- apollo-federation/cli/src/bench.rs | 2 +- apollo-federation/cli/src/main.rs | 5 +- apollo-federation/src/error/mod.rs | 2 - .../src/link/federation_spec_definition.rs | 32 ++ .../src/query_graph/build_query_graph.rs | 106 +++++ .../src/query_graph/graph_path.rs | 89 ++++- apollo-federation/src/query_graph/mod.rs | 64 ++- .../src/query_plan/query_planner.rs | 121 +++--- .../query_plan/query_planning_traversal.rs | 9 +- .../query_plan/build_query_plan_support.rs | 14 +- .../query_plan/build_query_plan_tests.rs | 2 +- .../fetch_operation_names.rs | 4 +- .../build_query_plan_tests/overrides.rs | 375 ++++++++++++++++++ .../overrides/shareable.rs | 244 ++++++++++++ ...ride_unset_labels_on_entity_fields.graphql | 73 ++++ ...set_labels_on_nested_entity_fields.graphql | 73 ++++ ...erride_unset_labels_on_root_fields.graphql | 53 +++ ...gressive_override_on_entity_fields.graphql | 73 ++++ ...e_override_on_nested_entity_fields.graphql | 73 ++++ ...rogressive_override_on_root_fields.graphql | 53 +++ ...es_f1_to_s3_when_label_is_provided.graphql | 66 +++ ...rides_to_s2_when_label_is_provided.graphql | 66 +++ ...1_in_s1_when_label_is_not_provided.graphql | 66 +++ ...s_in_s1_when_label_is_not_provided.graphql | 66 +++ .../cost_calculator/static_cost.rs | 4 +- .../src/query_planner/bridge_query_planner.rs | 40 +- .../src/query_planner/dual_query_planner.rs | 6 +- .../tests/integration/query_planner.rs | 106 ----- 28 files changed, 1657 insertions(+), 230 deletions(-) create mode 100644 apollo-federation/tests/query_plan/build_query_plan_tests/overrides.rs create mode 100644 apollo-federation/tests/query_plan/build_query_plan_tests/overrides/shareable.rs create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql diff --git a/apollo-federation/cli/src/bench.rs b/apollo-federation/cli/src/bench.rs index c672137982..ed135d8ba4 100644 --- a/apollo-federation/cli/src/bench.rs +++ b/apollo-federation/cli/src/bench.rs @@ -54,7 +54,7 @@ pub(crate) fn run_bench( } }; let now = Instant::now(); - let plan = planner.build_query_plan(&document, None); + let plan = planner.build_query_plan(&document, None, Default::default()); let elapsed = now.elapsed().as_secs_f64() * 1000.0; let mut eval_plans = None; let mut error = None; diff --git a/apollo-federation/cli/src/main.rs b/apollo-federation/cli/src/main.rs index ab42f16151..28bb5f7921 100644 --- a/apollo-federation/cli/src/main.rs +++ b/apollo-federation/cli/src/main.rs @@ -254,7 +254,10 @@ fn cmd_plan( let query_doc = ExecutableDocument::parse_and_validate(planner.api_schema().schema(), query, query_path)?; - print!("{}", planner.build_query_plan(&query_doc, None)?); + print!( + "{}", + planner.build_query_plan(&query_doc, None, Default::default())? + ); Ok(()) } diff --git a/apollo-federation/src/error/mod.rs b/apollo-federation/src/error/mod.rs index 9e4487c30f..d56c9783a6 100644 --- a/apollo-federation/src/error/mod.rs +++ b/apollo-federation/src/error/mod.rs @@ -33,8 +33,6 @@ impl From for String { #[derive(Clone, Debug, strum_macros::Display, PartialEq, Eq)] pub enum UnsupportedFeatureKind { - #[strum(to_string = "progressive overrides")] - ProgressiveOverrides, #[strum(to_string = "defer")] Defer, #[strum(to_string = "context")] diff --git a/apollo-federation/src/link/federation_spec_definition.rs b/apollo-federation/src/link/federation_spec_definition.rs index 67f181ec8b..184e93b690 100644 --- a/apollo-federation/src/link/federation_spec_definition.rs +++ b/apollo-federation/src/link/federation_spec_definition.rs @@ -12,6 +12,7 @@ use lazy_static::lazy_static; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::link::argument::directive_optional_boolean_argument; +use crate::link::argument::directive_optional_string_argument; use crate::link::argument::directive_required_string_argument; use crate::link::cost_spec_definition::CostSpecDefinition; use crate::link::cost_spec_definition::COST_VERSIONS; @@ -51,6 +52,11 @@ pub(crate) struct ProvidesDirectiveArguments<'doc> { pub(crate) fields: &'doc str, } +pub(crate) struct OverrideDirectiveArguments<'doc> { + pub(crate) from: &'doc str, + pub(crate) label: Option<&'doc str>, +} + #[derive(Debug)] pub(crate) struct FederationSpecDefinition { url: Url, @@ -361,6 +367,19 @@ impl FederationSpecDefinition { }) } + pub(crate) fn override_directive_definition<'schema>( + &self, + schema: &'schema FederationSchema, + ) -> Result<&'schema Node, FederationError> { + self.directive_definition(schema, &FEDERATION_OVERRIDE_DIRECTIVE_NAME_IN_SPEC)? + .ok_or_else(|| { + FederationError::internal(format!( + "Unexpectedly could not find federation spec's \"@{}\" directive definition", + FEDERATION_OVERRIDE_DIRECTIVE_NAME_IN_SPEC + )) + }) + } + pub(crate) fn override_directive( &self, schema: &FederationSchema, @@ -390,6 +409,19 @@ impl FederationSpecDefinition { }) } + pub(crate) fn override_directive_arguments<'doc>( + &self, + application: &'doc Node, + ) -> Result, FederationError> { + Ok(OverrideDirectiveArguments { + from: directive_required_string_argument(application, &FEDERATION_FROM_ARGUMENT_NAME)?, + label: directive_optional_string_argument( + application, + &FEDERATION_OVERRIDE_LABEL_ARGUMENT_NAME, + )?, + }) + } + pub(crate) fn get_cost_spec_definition( &self, schema: &FederationSchema, diff --git a/apollo-federation/src/query_graph/build_query_graph.rs b/apollo-federation/src/query_graph/build_query_graph.rs index 3dd7abbcd6..d4d2acf609 100644 --- a/apollo-federation/src/query_graph/build_query_graph.rs +++ b/apollo-federation/src/query_graph/build_query_graph.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use apollo_compiler::collections::HashMap; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::schema::DirectiveList as ComponentDirectiveList; @@ -21,6 +22,7 @@ use crate::link::federation_spec_definition::KeyDirectiveArguments; use crate::operation::merge_selection_sets; use crate::operation::Selection; use crate::operation::SelectionSet; +use crate::query_graph::OverrideCondition; use crate::query_graph::QueryGraph; use crate::query_graph::QueryGraphEdge; use crate::query_graph::QueryGraphEdgeTransition; @@ -140,6 +142,7 @@ impl BaseQueryGraphBuilder { QueryGraphEdge { transition, conditions, + override_condition: None, }, ); let head_weight = self.query_graph.node_weight(head)?; @@ -982,6 +985,7 @@ impl FederatedQueryGraphBuilder { self.add_root_edges()?; self.handle_key()?; self.handle_requires()?; + self.handle_progressive_overrides()?; // Note that @provides must be handled last when building since it requires copying nodes // and their edges, and it's easier to reason about this if we know previous self.handle_provides()?; @@ -1374,6 +1378,102 @@ impl FederatedQueryGraphBuilder { Ok(()) } + /// Handling progressive overrides here. For each progressive @override + /// application (with a label), we want to update the edges to the overridden + /// field within the "to" and "from" subgraphs with their respective override + /// condition (the label and a T/F value). The "from" subgraph will have an + /// override condition of `false`, whereas the "to" subgraph will have an + /// override condition of `true`. + fn handle_progressive_overrides(&mut self) -> Result<(), FederationError> { + let mut edge_to_conditions: HashMap = Default::default(); + + fn collect_edge_condition( + query_graph: &QueryGraph, + target_graph: &str, + target_field: &ObjectFieldDefinitionPosition, + label: &str, + condition: bool, + edge_to_conditions: &mut HashMap, + ) -> Result<(), FederationError> { + let target_field = FieldDefinitionPosition::Object(target_field.clone()); + let subgraph_nodes = query_graph + .types_to_nodes_by_source + .get(target_graph) + .unwrap(); + let parent_node = subgraph_nodes + .get(target_field.type_name()) + .unwrap() + .first() + .unwrap(); + for edge in query_graph.out_edges(*parent_node) { + let edge_weight = query_graph.edge_weight(edge.id())?; + let QueryGraphEdgeTransition::FieldCollection { + field_definition_position, + .. + } = &edge_weight.transition + else { + continue; + }; + + if &target_field == field_definition_position { + edge_to_conditions.insert( + edge.id(), + OverrideCondition { + label: label.to_string(), + condition, + }, + ); + } + } + Ok(()) + } + + for (to_subgraph_name, subgraph) in &self.base.query_graph.subgraphs_by_name { + let subgraph_data = self.subgraphs.get(to_subgraph_name)?; + if let Some(override_referencers) = subgraph + .referencers() + .directives + .get(&subgraph_data.overrides_directive_definition_name) + { + for field_definition_position in &override_referencers.object_fields { + let field = field_definition_position.get(subgraph.schema())?; + for directive in field + .directives + .get_all(&subgraph_data.overrides_directive_definition_name) + { + let application = subgraph_data + .federation_spec_definition + .override_directive_arguments(directive)?; + if let Some(label) = application.label { + collect_edge_condition( + &self.base.query_graph, + to_subgraph_name, + field_definition_position, + label, + true, + &mut edge_to_conditions, + )?; + collect_edge_condition( + &self.base.query_graph, + application.from, + field_definition_position, + label, + false, + &mut edge_to_conditions, + )?; + } + } + } + } + } + + for (edge, condition) in edge_to_conditions { + let mutable_edge = self.base.query_graph.edge_weight_mut(edge)?; + mutable_edge.override_condition = Some(condition); + } + Ok(()) + } + /// Handle @provides by copying the appropriate nodes/edges. fn handle_provides(&mut self) -> Result<(), FederationError> { let mut provide_id = 0; @@ -1987,6 +2087,10 @@ impl FederatedQueryGraphBuilderSubgraphs { ), } })?; + let overrides_directive_definition_name = federation_spec_definition + .override_directive_definition(schema)? + .name + .clone(); subgraphs.map.insert( source.clone(), FederatedQueryGraphBuilderSubgraphData { @@ -1995,6 +2099,7 @@ impl FederatedQueryGraphBuilderSubgraphs { requires_directive_definition_name, provides_directive_definition_name, interface_object_directive_definition_name, + overrides_directive_definition_name, }, ); } @@ -2020,6 +2125,7 @@ struct FederatedQueryGraphBuilderSubgraphData { requires_directive_definition_name: Name, provides_directive_definition_name: Name, interface_object_directive_definition_name: Name, + overrides_directive_definition_name: Name, } #[derive(Debug)] diff --git a/apollo-federation/src/query_graph/graph_path.rs b/apollo-federation/src/query_graph/graph_path.rs index c588f39d7c..88bba9e8a0 100644 --- a/apollo-federation/src/query_graph/graph_path.rs +++ b/apollo-federation/src/query_graph/graph_path.rs @@ -46,6 +46,7 @@ use crate::query_graph::path_tree::OpPathTree; use crate::query_graph::QueryGraph; use crate::query_graph::QueryGraphEdgeTransition; use crate::query_graph::QueryGraphNodeType; +use crate::query_plan::query_planner::EnabledOverrideConditions; use crate::query_plan::FetchDataPathElement; use crate::query_plan::QueryPathElement; use crate::query_plan::QueryPlanCost; @@ -1406,17 +1407,24 @@ where feature = "snapshot_tracing", tracing::instrument(skip_all, level = "trace") )] + #[allow(clippy::too_many_arguments)] fn advance_with_non_collecting_and_type_preserving_transitions( self: &Arc, context: &OpGraphPathContext, condition_resolver: &mut impl ConditionResolver, excluded_destinations: &ExcludedDestinations, excluded_conditions: &ExcludedConditions, + override_conditions: &EnabledOverrideConditions, transition_and_context_to_trigger: impl Fn( &QueryGraphEdgeTransition, &OpGraphPathContext, ) -> TTrigger, - node_and_trigger_to_edge: impl Fn(&Arc, NodeIndex, &Arc) -> Option, + node_and_trigger_to_edge: impl Fn( + &Arc, + NodeIndex, + &Arc, + &EnabledOverrideConditions, + ) -> Option, ) -> Result, FederationError> { // If we're asked for indirect paths after an "@interfaceObject fake down cast" but that // down cast comes just after non-collecting edge(s), then we can ignore the ask (skip @@ -1675,6 +1683,7 @@ where direct_path_start_node, edge_tail_type_pos, &node_and_trigger_to_edge, + override_conditions, )? } else { None @@ -1797,12 +1806,20 @@ where start_index: usize, start_node: NodeIndex, end_type_position: &OutputTypeDefinitionPosition, - node_and_trigger_to_edge: impl Fn(&Arc, NodeIndex, &Arc) -> Option, + node_and_trigger_to_edge: impl Fn( + &Arc, + NodeIndex, + &Arc, + &EnabledOverrideConditions, + ) -> Option, + override_conditions: &EnabledOverrideConditions, ) -> Result, FederationError> { let mut current_node = start_node; for index in start_index..self.edges.len() { let trigger = &self.edge_triggers[index]; - let Some(edge) = node_and_trigger_to_edge(&self.graph, current_node, trigger) else { + let Some(edge) = + node_and_trigger_to_edge(&self.graph, current_node, trigger, override_conditions) + else { return Ok(None); }; @@ -1892,8 +1909,13 @@ where } impl OpGraphPath { - fn next_edge_for_field(&self, field: &Field) -> Option { - self.graph.edge_for_field(self.tail, field) + fn next_edge_for_field( + &self, + field: &Field, + override_conditions: &EnabledOverrideConditions, + ) -> Option { + self.graph + .edge_for_field(self.tail, field, override_conditions) } fn next_edge_for_inline_fragment(&self, inline_fragment: &InlineFragment) -> Option { @@ -2027,6 +2049,7 @@ impl OpGraphPath { pub(crate) fn terminate_with_non_requested_typename_field( &self, + override_conditions: &EnabledOverrideConditions, ) -> Result { // If the last step of the path was a fragment/type-condition, we want to remove it before // we get __typename. The reason is that this avoid cases where this method would make us @@ -2066,7 +2089,10 @@ impl OpGraphPath { &tail_type_pos, None, ); - let Some(edge) = self.graph.edge_for_field(path.tail, &typename_field) else { + let Some(edge) = self + .graph + .edge_for_field(path.tail, &typename_field, override_conditions) + else { return Err(FederationError::internal( "Unexpectedly missing edge for __typename field", )); @@ -2401,6 +2427,7 @@ impl OpGraphPath { operation_element: &OpPathElement, context: &OpGraphPathContext, condition_resolver: &mut impl ConditionResolver, + override_conditions: &EnabledOverrideConditions, ) -> Result<(Option>, Option), FederationError> { let span = debug_span!("Trying to advance {self} directly with {operation_element}"); let _guard = span.enter(); @@ -2417,7 +2444,9 @@ impl OpGraphPath { OutputTypeDefinitionPosition::Object(tail_type_pos) => { // Just take the edge corresponding to the field, if it exists and can be // used. - let Some(edge) = self.next_edge_for_field(operation_field) else { + let Some(edge) = + self.next_edge_for_field(operation_field, override_conditions) + else { debug!( "No edge for field {operation_field} on object type {tail_weight}" ); @@ -2504,7 +2533,7 @@ impl OpGraphPath { let interface_edge = if field_is_of_an_implementation { None } else { - self.next_edge_for_field(operation_field) + self.next_edge_for_field(operation_field, override_conditions) }; let interface_path = if let Some(interface_edge) = &interface_edge { let field_path = self.add_field_edge( @@ -2668,6 +2697,7 @@ impl OpGraphPath { supergraph_schema.clone(), &implementation_inline_fragment.into(), condition_resolver, + override_conditions, )?; // If we find no options for that implementation, we bail (as we need to // simultaneously advance all implementations). @@ -2697,6 +2727,7 @@ impl OpGraphPath { supergraph_schema.clone(), operation_element, condition_resolver, + override_conditions, )?; let Some(field_options_for_implementation) = field_options_for_implementation @@ -2756,7 +2787,9 @@ impl OpGraphPath { } } OutputTypeDefinitionPosition::Union(_) => { - let Some(typename_edge) = self.next_edge_for_field(operation_field) else { + let Some(typename_edge) = + self.next_edge_for_field(operation_field, override_conditions) + else { return Err(FederationError::internal( "Should always have an edge for __typename edge on an union", )); @@ -2871,6 +2904,7 @@ impl OpGraphPath { supergraph_schema.clone(), &implementation_inline_fragment.into(), condition_resolver, + override_conditions, )?; let Some(implementation_options) = implementation_options else { drop(guard); @@ -3277,6 +3311,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { updated_context: &OpGraphPathContext, path_index: usize, condition_resolver: &mut impl ConditionResolver, + override_conditions: &EnabledOverrideConditions, ) -> Result { // Note that the provided context will usually be one we had during construction (the // `updated_context` will be `self.context` updated by whichever operation we're looking at, @@ -3284,12 +3319,13 @@ impl SimultaneousPathsWithLazyIndirectPaths { // rare), which is why we save recomputation by caching the computed value in that case, but // in case it's different, we compute without caching. if *updated_context != self.context { - self.compute_indirect_paths(path_index, condition_resolver)?; + self.compute_indirect_paths(path_index, condition_resolver, override_conditions)?; } if let Some(indirect_paths) = &self.lazily_computed_indirect_paths[path_index] { Ok(indirect_paths.clone()) } else { - let new_indirect_paths = self.compute_indirect_paths(path_index, condition_resolver)?; + let new_indirect_paths = + self.compute_indirect_paths(path_index, condition_resolver, override_conditions)?; self.lazily_computed_indirect_paths[path_index] = Some(new_indirect_paths.clone()); Ok(new_indirect_paths) } @@ -3299,17 +3335,21 @@ impl SimultaneousPathsWithLazyIndirectPaths { &self, path_index: usize, condition_resolver: &mut impl ConditionResolver, + overridden_conditions: &EnabledOverrideConditions, ) -> Result { self.paths.0[path_index].advance_with_non_collecting_and_type_preserving_transitions( &self.context, condition_resolver, &self.excluded_destinations, &self.excluded_conditions, + overridden_conditions, // The transitions taken by this method are non-collecting transitions, in which case // the trigger is the context (which is really a hack to provide context information for // keys during fetch dependency graph updating). |_, context| OpGraphPathTrigger::Context(context.clone()), - |graph, node, trigger| graph.edge_for_op_graph_path_trigger(node, trigger), + |graph, node, trigger, overridden_conditions| { + graph.edge_for_op_graph_path_trigger(node, trigger, overridden_conditions) + }, ) } @@ -3345,6 +3385,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { supergraph_schema: ValidFederationSchema, operation_element: &OpPathElement, condition_resolver: &mut impl ConditionResolver, + override_conditions: &EnabledOverrideConditions, ) -> Result>, FederationError> { debug!( "Trying to advance paths for operation: path = {}, operation = {operation_element}", @@ -3375,6 +3416,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { operation_element, &updated_context, condition_resolver, + override_conditions, )?; debug!("{advance_options:?}"); drop(gaurd); @@ -3422,7 +3464,12 @@ impl SimultaneousPathsWithLazyIndirectPaths { if let OpPathElement::Field(operation_field) = operation_element { // Add whatever options can be obtained by taking some non-collecting edges first. let paths_with_non_collecting_edges = self - .indirect_options(&updated_context, path_index, condition_resolver)? + .indirect_options( + &updated_context, + path_index, + condition_resolver, + override_conditions, + )? .filter_non_collecting_paths_for_field(operation_field)?; if !paths_with_non_collecting_edges.paths.is_empty() { debug!( @@ -3441,6 +3488,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { operation_element, &updated_context, condition_resolver, + override_conditions, )?; // If we can't advance the operation element after that path, ignore it, // it's just not an option. @@ -3528,6 +3576,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { operation_element, &updated_context, condition_resolver, + override_conditions, )?; options = advance_options.unwrap_or_else(Vec::new); debug!("{options:?}"); @@ -3552,11 +3601,6 @@ impl SimultaneousPathsWithLazyIndirectPaths { // PORT_NOTE: JS passes a ConditionResolver here, we do not: see port note for // `SimultaneousPathsWithLazyIndirectPaths` -// TODO(@goto-bus-stop): JS passes `override_conditions` here and maintains stores -// references to it in the created paths. AFAICT override conditions -// are shared mutable state among different query graphs, so having references to -// it in many structures would require synchronization. We should likely pass it as -// an argument to exactly the functionality that uses it. pub fn create_initial_options( initial_path: GraphPath>, initial_type: &QueryGraphNodeType, @@ -3564,6 +3608,7 @@ pub fn create_initial_options( condition_resolver: &mut impl ConditionResolver, excluded_edges: ExcludedDestinations, excluded_conditions: ExcludedConditions, + override_conditions: &EnabledOverrideConditions, ) -> Result, FederationError> { let initial_paths = SimultaneousPaths::from(initial_path); let mut lazy_initial_path = SimultaneousPathsWithLazyIndirectPaths::new( @@ -3574,8 +3619,12 @@ pub fn create_initial_options( ); if initial_type.is_federated_root_type() { - let initial_options = - lazy_initial_path.indirect_options(&initial_context, 0, condition_resolver)?; + let initial_options = lazy_initial_path.indirect_options( + &initial_context, + 0, + condition_resolver, + override_conditions, + )?; let options = initial_options .paths .iter() diff --git a/apollo-federation/src/query_graph/mod.rs b/apollo-federation/src/query_graph/mod.rs index f95588446c..4060ffdbe6 100644 --- a/apollo-federation/src/query_graph/mod.rs +++ b/apollo-federation/src/query_graph/mod.rs @@ -44,6 +44,7 @@ use crate::query_graph::graph_path::ExcludedDestinations; use crate::query_graph::graph_path::OpGraphPathContext; use crate::query_graph::graph_path::OpGraphPathTrigger; use crate::query_graph::graph_path::OpPathElement; +use crate::query_plan::query_planner::EnabledOverrideConditions; use crate::query_plan::QueryPlanCost; #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -147,6 +148,25 @@ pub(crate) struct QueryGraphEdge { /// /// Outside of keys, @requires edges also rely on conditions. pub(crate) conditions: Option>, + /// Edges can require that an override condition (provided during query + /// planning) be met in order to be taken. This is used for progressive + /// @override, where (at least) 2 subgraphs can resolve the same field, but + /// one of them has an @override with a label. If the override condition + /// matches the query plan parameters, this edge can be taken. + pub(crate) override_condition: Option, +} + +impl QueryGraphEdge { + fn satisfies_override_conditions( + &self, + conditions_to_check: &EnabledOverrideConditions, + ) -> bool { + if let Some(override_condition) = &self.override_condition { + override_condition.condition == conditions_to_check.contains(&override_condition.label) + } else { + true + } + } } impl Display for QueryGraphEdge { @@ -158,13 +178,32 @@ impl Display for QueryGraphEdge { { return Ok(()); } - if let Some(conditions) = &self.conditions { - write!(f, "{} ⊢ {}", conditions, self.transition) - } else { - self.transition.fmt(f) + + match (&self.override_condition, &self.conditions) { + (Some(override_condition), Some(conditions)) => write!( + f, + "{}, {} ⊢ {}", + conditions, override_condition, self.transition + ), + (Some(override_condition), None) => { + write!(f, "{} ⊢ {}", override_condition, self.transition) + } + (None, Some(conditions)) => write!(f, "{} ⊢ {}", conditions, self.transition), + _ => self.transition.fmt(f), } } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct OverrideCondition { + pub(crate) label: String, + pub(crate) condition: bool, +} + +impl Display for OverrideCondition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{} = {}", self.label, self.condition) + } +} /// The type of query graph edge "transition". /// @@ -639,7 +678,12 @@ impl QueryGraph { .find_ok(|selection| !external_metadata.selects_any_external_field(selection)) } - pub(crate) fn edge_for_field(&self, node: NodeIndex, field: &Field) -> Option { + pub(crate) fn edge_for_field( + &self, + node: NodeIndex, + field: &Field, + override_conditions: &EnabledOverrideConditions, + ) -> Option { let mut candidates = self.out_edges(node).into_iter().filter_map(|edge_ref| { let edge_weight = edge_ref.weight(); let QueryGraphEdgeTransition::FieldCollection { @@ -649,6 +693,11 @@ impl QueryGraph { else { return None; }; + + if !edge_weight.satisfies_override_conditions(override_conditions) { + return None; + } + // We explicitly avoid comparing parent type's here, to allow interface object // fields to match operation fields with the same name but differing types. if field.field_position.field_name() == field_definition_position.field_name() { @@ -715,12 +764,15 @@ impl QueryGraph { &self, node: NodeIndex, op_graph_path_trigger: &OpGraphPathTrigger, + override_conditions: &EnabledOverrideConditions, ) -> Option> { let OpGraphPathTrigger::OpPathElement(op_path_element) = op_graph_path_trigger else { return None; }; match op_path_element { - OpPathElement::Field(field) => self.edge_for_field(node, field).map(Some), + OpPathElement::Field(field) => self + .edge_for_field(node, field, override_conditions) + .map(Some), OpPathElement::InlineFragment(inline_fragment) => { if inline_fragment.type_condition_position.is_some() { self.edge_for_inline_fragment(node, inline_fragment) diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index c2c2d75805..a64f79364c 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -1,10 +1,11 @@ use std::cell::Cell; use std::num::NonZeroU32; +use std::ops::Deref; use std::sync::Arc; +use apollo_compiler::collections::HashSet; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; -use apollo_compiler::schema::ExtendedType; use apollo_compiler::validation::Valid; use apollo_compiler::ExecutableDocument; use apollo_compiler::Name; @@ -48,7 +49,6 @@ use crate::utils::logging::snapshot; use crate::ApiSchemaOptions; use crate::Supergraph; -pub(crate) const OVERRIDE_LABEL_ARG_NAME: &str = "overrideLabel"; pub(crate) const CONTEXT_DIRECTIVE: &str = "context"; pub(crate) const JOIN_FIELD: &str = "join__field"; @@ -196,6 +196,28 @@ impl QueryPlannerConfig { } } +#[derive(Debug, Default, Clone)] +pub struct QueryPlanOptions { + /// A set of labels which will be used _during query planning_ to + /// enable/disable edges with a matching label in their override condition. + /// Edges with override conditions require their label to be present or absent + /// from this set in order to be traversable. These labels enable the + /// progressive @override feature. + // PORT_NOTE: In JS implementation this was a Map + pub override_conditions: Vec, +} + +#[derive(Debug, Default, Clone)] +pub(crate) struct EnabledOverrideConditions(HashSet); + +impl Deref for EnabledOverrideConditions { + type Target = HashSet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + pub struct QueryPlanner { config: QueryPlannerConfig, federated_query_graph: Arc, @@ -308,11 +330,6 @@ impl QueryPlanner { .filter(|position| is_inconsistent(position.clone())) .collect::>(); - // PORT_NOTE: JS prepares a map of override conditions here, which is - // a map where the keys are all `@join__field(overrideLabel:)` argument values - // and the values are all initialised to `false`. Instead of doing that, we should - // be able to use a Set where presence means `true` and absence means `false`. - Ok(Self { config, federated_query_graph: Arc::new(query_graph), @@ -339,6 +356,7 @@ impl QueryPlanner { &self, document: &Valid, operation_name: Option, + options: QueryPlanOptions, ) -> Result { let operation = document .operations @@ -482,7 +500,9 @@ impl QueryPlanner { .clone() .into(), config: self.config.clone(), - // PORT_NOTE: JS provides `override_conditions` here: see port note in `QueryPlanner::new`. + override_conditions: EnabledOverrideConditions(HashSet::from_iter( + options.override_conditions, + )), }; let root_node = match defer_conditions { @@ -549,59 +569,6 @@ impl QueryPlanner { } fn check_unsupported_features(supergraph: &Supergraph) -> Result<(), FederationError> { - // We have a *progressive* override when `join__field` has a - // non-null value for `overrideLabel` field. - // - // This looks at object types' fields and their directive - // applications, looking specifically for `@join__field` - // arguments list. - let has_progressive_overrides = supergraph - .schema - .schema() - .types - .values() - .filter_map(|extended_type| { - // The override label args can be only on ObjectTypes - if let ExtendedType::Object(object_type) = extended_type { - Some(object_type) - } else { - None - } - }) - .flat_map(|object_type| &object_type.fields) - .flat_map(|(_, field)| { - field - .directives - .iter() - .filter(|d| d.name.as_str() == JOIN_FIELD) - }) - .any(|join_directive| { - if let Some(override_label_arg) = - join_directive.argument_by_name(OVERRIDE_LABEL_ARG_NAME) - { - // Any argument value for `overrideLabel` that's not - // null can be considered as progressive override usage - if !override_label_arg.is_null() { - return true; - } - return false; - } - false - }); - if has_progressive_overrides { - let message = "\ - `experimental_query_planner_mode: new` or `both` cannot yet \ - be used with progressive overrides. \ - Remove uses of progressive overrides to try the experimental query planner, \ - otherwise switch back to `legacy` or `both_best_effort`.\ - "; - return Err(SingleFederationError::UnsupportedFeature { - message: message.to_owned(), - kind: crate::error::UnsupportedFeatureKind::ProgressiveOverrides, - } - .into()); - } - // We will only check for `@context` direcive, since // `@fromContext` can only be used if `@context` is already // applied, and we assume a correctly composed supergraph. @@ -1102,7 +1069,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "accounts") { @@ -1134,7 +1103,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Sequence { @@ -1223,7 +1194,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Parallel { @@ -1334,7 +1307,9 @@ type User let mut config = QueryPlannerConfig::default(); config.debug.bypass_planner_for_single_subgraph = true; let planner = QueryPlanner::new(&supergraph, config).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "A") { @@ -1378,7 +1353,9 @@ type User .unwrap(); let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "accounts") { @@ -1437,7 +1414,9 @@ type User .unwrap(); let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "accounts") { @@ -1497,7 +1476,9 @@ type User .unwrap(); let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); // Make sure `fragment F2` contains `...F1`. insta::assert_snapshot!(plan, @r###" QueryPlan { @@ -1554,7 +1535,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "Subgraph1") { diff --git a/apollo-federation/src/query_plan/query_planning_traversal.rs b/apollo-federation/src/query_plan/query_planning_traversal.rs index c54a81ba66..cb5e6bb9f1 100644 --- a/apollo-federation/src/query_plan/query_planning_traversal.rs +++ b/apollo-federation/src/query_plan/query_planning_traversal.rs @@ -36,6 +36,7 @@ use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphToC use crate::query_plan::generate::generate_all_plans_and_find_best; use crate::query_plan::generate::PlanBuilder; use crate::query_plan::query_planner::compute_root_fetch_groups; +use crate::query_plan::query_planner::EnabledOverrideConditions; use crate::query_plan::query_planner::QueryPlannerConfig; use crate::query_plan::query_planner::QueryPlanningStatistics; use crate::query_plan::QueryPlanCost; @@ -72,6 +73,7 @@ pub(crate) struct QueryPlanningParameters<'a> { /// The configuration for the query planner. pub(crate) config: QueryPlannerConfig, pub(crate) statistics: &'a QueryPlanningStatistics, + pub(crate) override_conditions: EnabledOverrideConditions, } pub(crate) struct QueryPlanningTraversal<'a, 'b> { @@ -247,6 +249,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { &mut traversal, excluded_destinations, excluded_conditions, + ¶meters.override_conditions, )?; traversal.open_branches = map_options_to_selections(selection_set, initial_options); @@ -337,6 +340,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { self.parameters.supergraph_schema.clone(), &operation_element, /*resolver*/ self, + &self.parameters.override_conditions, )?; let Some(followups_for_option) = followups_for_option else { // There is no valid way to advance the current operation element from this option @@ -404,7 +408,9 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { let mut new_simultaneous_paths = vec![]; for simultaneous_path in &option.paths.0 { new_simultaneous_paths.push(Arc::new( - simultaneous_path.terminate_with_non_requested_typename_field()?, + simultaneous_path.terminate_with_non_requested_typename_field( + &self.parameters.override_conditions, + )?, )); } closed_paths.push(Arc::new(ClosedPath { @@ -1054,6 +1060,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { .clone(), config: self.parameters.config.clone(), statistics: self.parameters.statistics, + override_conditions: self.parameters.override_conditions.clone(), }; let best_plan_opt = QueryPlanningTraversal::new_inner( ¶meters, diff --git a/apollo-federation/tests/query_plan/build_query_plan_support.rs b/apollo-federation/tests/query_plan/build_query_plan_support.rs index ed70798f2f..c063dbb3d7 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_support.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_support.rs @@ -60,6 +60,18 @@ macro_rules! subgraph_name { /// formatted query plan string. /// Run `cargo insta review` to diff and accept changes to the generated query plan. macro_rules! assert_plan { + ($api_schema_and_planner: expr, $operation: expr, $options: expr, @$expected: literal) => {{ + let (api_schema, planner) = $api_schema_and_planner; + let document = apollo_compiler::ExecutableDocument::parse_and_validate( + api_schema.schema(), + $operation, + "operation.graphql", + ) + .unwrap(); + let plan = planner.build_query_plan(&document, None, $options).unwrap(); + insta::assert_snapshot!(plan, @$expected); + plan + }}; ($api_schema_and_planner: expr, $operation: expr, @$expected: literal) => {{ let (api_schema, planner) = $api_schema_and_planner; let document = apollo_compiler::ExecutableDocument::parse_and_validate( @@ -68,7 +80,7 @@ macro_rules! assert_plan { "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner.build_query_plan(&document, None, Default::default()).unwrap(); insta::assert_snapshot!(plan, @$expected); plan }}; 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 0d618a6d0f..acef092c2c 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests.rs @@ -44,11 +44,11 @@ mod merged_abstract_types_handling; mod mutations; mod named_fragments; mod named_fragments_preservation; +mod overrides; mod provides; mod requires; mod shareable_root_fields; mod subscriptions; - // TODO: port the rest of query-planner-js/src/__tests__/buildPlan.test.ts #[test] diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs index a18565aed0..884c10e7e1 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs @@ -253,7 +253,9 @@ fn correctly_handle_case_where_there_is_too_many_plans_to_consider() { "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); // Note: The way the code that handle multiple plans currently work, it mess up the order of fields a bit. It's not a // big deal in practice cause everything gets re-order in practice during actual execution, but this means it's a tad diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/overrides.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides.rs new file mode 100644 index 0000000000..c0f1b91a31 --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides.rs @@ -0,0 +1,375 @@ +use apollo_federation::query_plan::query_planner::QueryPlanOptions; + +mod shareable; + +#[test] +fn it_handles_progressive_override_on_root_fields() { + let planner = planner!( + s1: r#" + type Query { + hello: String + } + "#, + s2: r#" + type Query { + hello: String @override(from: "s1", label: "test") + } + "#, + ); + assert_plan!( + &planner, + r#" + { + hello + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Fetch(service: "s2") { + { + hello + } + }, + } + "### + ); +} + +#[test] +fn it_does_not_override_unset_labels_on_root_fields() { + let planner = planner!( + s1: r#" + type Query { + hello: String + } + "#, + s2: r#" + type Query { + hello: String @override(from: "s1", label: "test") + } + "#, + ); + assert_plan!( + &planner, + r#" + { + hello + } + "#, + + @r###" + QueryPlan { + Fetch(service: "s1") { + { + hello + } + }, + } + "### + ); +} + +#[test] +fn it_handles_progressive_override_on_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "s2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f1 + f2 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_does_not_override_unset_labels_on_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + + @r###" + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + t { + __typename + id + f1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "s2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f2 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_handles_progressive_override_on_nested_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t2 { + t { + f1 + } + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + t2 { + t { + __typename + id + } + } + } + }, + Flatten(path: "t2.t") { + Fetch(service: "s2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f1 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_does_not_override_unset_labels_on_nested_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t2 { + t { + f1 + } + } + } + "#, + + @r###" + QueryPlan { + Fetch(service: "s1") { + { + t2 { + t { + f1 + } + } + } + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/overrides/shareable.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides/shareable.rs new file mode 100644 index 0000000000..103017912e --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides/shareable.rs @@ -0,0 +1,244 @@ +use apollo_federation::query_plan::query_planner::QueryPlanOptions; + +const S1: &str = r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + f1: String @shareable + } +"#; + +const S2: &str = r#" + type T @key(fields: "id") { + id: ID! + f1: String @shareable @override(from: "S1", label: "test") + f2: String + } +"#; + +const S3: &str = r#" + type T @key(fields: "id") { + id: ID! + f1: String @shareable + f3: String + } +"#; + +#[test] +fn it_overrides_to_s2_when_label_is_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "S2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f2 + f1 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_resolves_in_s1_when_label_is_not_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + f1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "S2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f2 + } + } + }, + }, + }, + } + "### + ); +} + +// This is very similar to the S2 example. The fact that the @override in S2 +// specifies _from_ S1 actually affects all T.f1 fields the same way (except +// S1). That is to say, it's functionally equivalent to have the `@override` +// exist in either S2 or S3 from S2/S3/Sn's perspective. It's helpful to +// test here that the QP will take a path through _either_ S2 or S3 when +// appropriate to do so. In these tests and the previous S2 tests, +// "appropriate" is determined by the other fields being selected in the +// query. +#[test] +fn it_overrides_f1_to_s3_when_label_is_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f3 + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "S3") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f3 + f1 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_resolves_f1_in_s1_when_label_is_not_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f3 + } + } + "#, + + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + f1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "S3") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f3 + } + } + }, + }, + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql new file mode 100644 index 0000000000..206487ca49 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql @@ -0,0 +1,53 @@ +# Composed from subgraphs with hash: b409284c35003f62c7c83675734478b1970effb2 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + hello: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql new file mode 100644 index 0000000000..206487ca49 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql @@ -0,0 +1,53 @@ +# Composed from subgraphs with hash: b409284c35003f62c7c83675734478b1970effb2 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + hello: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs index 7cf8b1ba4a..752479b256 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs @@ -725,7 +725,9 @@ mod tests { let planner = QueryPlanner::new(schema.federation_supergraph(), Default::default()).unwrap(); - let query_plan = planner.build_query_plan(&query.executable, None).unwrap(); + let query_plan = planner + .build_query_plan(&query.executable, None, Default::default()) + .unwrap(); let schema = DemandControlledSchema::new(Arc::new(schema.supergraph_schema().clone())).unwrap(); diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 24ad85ee17..9cab43aab8 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -12,6 +12,7 @@ use apollo_compiler::validation::Valid; use apollo_compiler::Name; use apollo_federation::error::FederationError; use apollo_federation::error::SingleFederationError; +use apollo_federation::query_plan::query_planner::QueryPlanOptions; use apollo_federation::query_plan::query_planner::QueryPlanner; use futures::future::BoxFuture; use opentelemetry_api::metrics::MeterProvider as _; @@ -68,7 +69,6 @@ use crate::Configuration; pub(crate) const RUST_QP_MODE: &str = "rust"; pub(crate) const JS_QP_MODE: &str = "js"; const UNSUPPORTED_CONTEXT: &str = "context"; -const UNSUPPORTED_OVERRIDES: &str = "overrides"; const UNSUPPORTED_FED1: &str = "fed1"; const INTERNAL_INIT_ERROR: &str = "internal"; @@ -203,9 +203,6 @@ impl PlannerMode { metric_rust_qp_init(Some(UNSUPPORTED_FED1)); } SingleFederationError::UnsupportedFeature { message: _, kind } => match kind { - apollo_federation::error::UnsupportedFeatureKind::ProgressiveOverrides => { - metric_rust_qp_init(Some(UNSUPPORTED_OVERRIDES)) - } apollo_federation::error::UnsupportedFeatureKind::Context => { metric_rust_qp_init(Some(UNSUPPORTED_CONTEXT)) } @@ -298,12 +295,20 @@ impl PlannerMode { let (plan, mut root_node) = tokio::task::spawn_blocking(move || { let start = Instant::now(); + let query_plan_options = QueryPlanOptions { + override_conditions: plan_options.override_conditions, + }; + let result = operation .as_deref() .map(|n| Name::new(n).map_err(FederationError::from)) .transpose() .and_then(|operation| { - rust_planner.build_query_plan(&doc.executable, operation) + rust_planner.build_query_plan( + &doc.executable, + operation, + query_plan_options, + ) }) .map_err(|e| QueryPlannerError::FederationError(e.to_string())); @@ -346,7 +351,7 @@ impl PlannerMode { let start = Instant::now(); let result = js - .plan(filtered_query, operation.clone(), plan_options) + .plan(filtered_query, operation.clone(), plan_options.clone()) .await; let elapsed = start.elapsed().as_secs_f64(); @@ -365,6 +370,9 @@ impl PlannerMode { } } + let query_plan_options = QueryPlanOptions { + override_conditions: plan_options.override_conditions, + }; BothModeComparisonJob { rust_planner: rust.clone(), js_duration: elapsed, @@ -375,6 +383,7 @@ impl PlannerMode { .as_ref() .map(|success| success.data.clone()) .map_err(|e| e.errors.clone()), + plan_options: query_plan_options, } .schedule(); @@ -941,9 +950,9 @@ impl BridgeQueryPlanner { if has_schema_introspection { if has_other_root_fields { let error = graphql::Error::builder() - .message("Mixed queries with both schema introspection and concrete fields are not supported") - .extension_code("MIXED_INTROSPECTION") - .build(); + .message("Mixed queries with both schema introspection and concrete fields are not supported") + .extension_code("MIXED_INTROSPECTION") + .build(); return Ok(QueryPlannerContent::Response { response: Box::new(graphql::Response::builder().error(error).build()), }); @@ -1242,8 +1251,8 @@ mod tests { &doc, query_metrics ) - .await - .unwrap_err(); + .await + .unwrap_err(); match err { QueryPlannerError::EmptyPlan(usage_reporting) => { @@ -1347,7 +1356,7 @@ mod tests { } }}"#); // Aliases - // FIXME: uncomment myName alias when this is fixed: + // FIXME: uncomment myName alias when this is fixed: // https://github.com/apollographql/router/issues/3263 s!(r#"query Q { me { username @@ -1797,13 +1806,6 @@ mod tests { "init.error_kind" = "context", "init.is_success" = false ); - metric_rust_qp_init(Some(UNSUPPORTED_OVERRIDES)); - assert_counter!( - "apollo.router.lifecycle.query_planner.init", - 1, - "init.error_kind" = "overrides", - "init.is_success" = false - ); metric_rust_qp_init(Some(UNSUPPORTED_FED1)); assert_counter!( "apollo.router.lifecycle.query_planner.init", diff --git a/apollo-router/src/query_planner/dual_query_planner.rs b/apollo-router/src/query_planner/dual_query_planner.rs index e368582a97..9952eb9782 100644 --- a/apollo-router/src/query_planner/dual_query_planner.rs +++ b/apollo-router/src/query_planner/dual_query_planner.rs @@ -12,6 +12,7 @@ use apollo_compiler::ast; use apollo_compiler::validation::Valid; use apollo_compiler::ExecutableDocument; use apollo_compiler::Name; +use apollo_federation::query_plan::query_planner::QueryPlanOptions; use apollo_federation::query_plan::query_planner::QueryPlanner; use apollo_federation::query_plan::QueryPlan; @@ -43,6 +44,7 @@ pub(crate) struct BothModeComparisonJob { pub(crate) document: Arc>, pub(crate) operation_name: Option, pub(crate) js_result: Result>>, + pub(crate) plan_options: QueryPlanOptions, } type Queue = crossbeam_channel::Sender; @@ -88,7 +90,9 @@ impl BothModeComparisonJob { let start = Instant::now(); // No question mark operator or macro from here … - let result = self.rust_planner.build_query_plan(&self.document, name); + let result = + self.rust_planner + .build_query_plan(&self.document, name, self.plan_options); let elapsed = start.elapsed().as_secs_f64(); metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); diff --git a/apollo-router/tests/integration/query_planner.rs b/apollo-router/tests/integration/query_planner.rs index 88ace09147..03056ab47d 100644 --- a/apollo-router/tests/integration/query_planner.rs +++ b/apollo-router/tests/integration/query_planner.rs @@ -158,112 +158,6 @@ async fn fed2_schema_with_new_qp() { router.graceful_shutdown().await; } -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_legacy_qp() { - if !graph_os_enabled() { - return; - } - let mut router = IntegrationTest::builder() - .config(LEGACY_QP) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_new_qp() { - if !graph_os_enabled() { - return; - } - let mut router = IntegrationTest::builder() - .config(NEW_QP) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router - .assert_log_contains( - "could not create router: \ - failed to initialize the query planner: \ - `experimental_query_planner_mode: new` or `both` cannot yet \ - be used with progressive overrides. \ - Remove uses of progressive overrides to try the experimental query planner, \ - otherwise switch back to `legacy` or `both_best_effort`.", - ) - .await; - router.assert_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_legacy_qp_change_to_new_qp_keeps_old_config() { - if !graph_os_enabled() { - return; - } - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); - let mut router = IntegrationTest::builder() - .config(config) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{NEW_QP}"); - router.update_config(&config).await; - router - .assert_log_contains("error while reloading, continuing with previous configuration") - .await; - router - .assert_metrics_contains( - r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="overrides",init_is_success="false",otel_scope_name="apollo/router"} 1"#, - None, - ) - .await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_legacy_qp_reload_to_both_best_effort_keep_previous_config() { - if !graph_os_enabled() { - return; - } - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); - let mut router = IntegrationTest::builder() - .config(config) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{BOTH_BEST_EFFORT_QP}"); - router.update_config(&config).await; - router - .assert_log_contains( - "Falling back to the legacy query planner: \ - failed to initialize the query planner: \ - `experimental_query_planner_mode: new` or `both` cannot yet \ - be used with progressive overrides. \ - Remove uses of progressive overrides to try the experimental query planner, \ - otherwise switch back to `legacy` or `both_best_effort`.", - ) - .await; - router - .assert_metrics_contains( - r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="overrides",init_is_success="false",otel_scope_name="apollo/router"} 1"#, - None, - ) - .await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - #[tokio::test(flavor = "multi_thread")] async fn context_with_legacy_qp() { if !graph_os_enabled() { From 7719616803d4552b1ae9fd99c5b78b3ecc00dc32 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 19 Sep 2024 17:04:17 +0300 Subject: [PATCH 37/56] prep release: v1.55.0-rc.1 --- Cargo.lock | 8 ++++---- apollo-federation/Cargo.toml | 2 +- apollo-router-benchmarks/Cargo.toml | 2 +- apollo-router-scaffold/Cargo.toml | 2 +- apollo-router-scaffold/templates/base/Cargo.template.toml | 2 +- .../templates/base/xtask/Cargo.template.toml | 2 +- apollo-router/Cargo.toml | 4 ++-- dockerfiles/tracing/docker-compose.datadog.yml | 2 +- dockerfiles/tracing/docker-compose.jaeger.yml | 2 +- dockerfiles/tracing/docker-compose.zipkin.yml | 2 +- helm/chart/router/Chart.yaml | 4 ++-- helm/chart/router/README.md | 6 +++--- licenses.html | 5 +---- scripts/install.sh | 2 +- 14 files changed, 21 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3a1b1819f..1bc0a10bba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,7 +178,7 @@ dependencies = [ [[package]] name = "apollo-federation" -version = "1.55.0-rc.0" +version = "1.55.0-rc.1" dependencies = [ "apollo-compiler", "derive_more", @@ -229,7 +229,7 @@ dependencies = [ [[package]] name = "apollo-router" -version = "1.55.0-rc.0" +version = "1.55.0-rc.1" dependencies = [ "access-json", "ahash", @@ -401,7 +401,7 @@ dependencies = [ [[package]] name = "apollo-router-benchmarks" -version = "1.55.0-rc.0" +version = "1.55.0-rc.1" dependencies = [ "apollo-parser", "apollo-router", @@ -417,7 +417,7 @@ dependencies = [ [[package]] name = "apollo-router-scaffold" -version = "1.55.0-rc.0" +version = "1.55.0-rc.1" dependencies = [ "anyhow", "cargo-scaffold", diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml index 2f11c95349..da555587f4 100644 --- a/apollo-federation/Cargo.toml +++ b/apollo-federation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-federation" -version = "1.55.0-rc.0" +version = "1.55.0-rc.1" authors = ["The Apollo GraphQL Contributors"] edition = "2021" description = "Apollo Federation" diff --git a/apollo-router-benchmarks/Cargo.toml b/apollo-router-benchmarks/Cargo.toml index 632fe52e8d..396c62a76b 100644 --- a/apollo-router-benchmarks/Cargo.toml +++ b/apollo-router-benchmarks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-benchmarks" -version = "1.55.0-rc.0" +version = "1.55.0-rc.1" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/Cargo.toml b/apollo-router-scaffold/Cargo.toml index e02093dd24..bbc2f33351 100644 --- a/apollo-router-scaffold/Cargo.toml +++ b/apollo-router-scaffold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-scaffold" -version = "1.55.0-rc.0" +version = "1.55.0-rc.1" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/templates/base/Cargo.template.toml b/apollo-router-scaffold/templates/base/Cargo.template.toml index 9b38fbb051..2a3940cf81 100644 --- a/apollo-router-scaffold/templates/base/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/Cargo.template.toml @@ -22,7 +22,7 @@ apollo-router = { path ="{{integration_test}}apollo-router" } apollo-router = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} # Note if you update these dependencies then also update xtask/Cargo.toml -apollo-router = "1.55.0-rc.0" +apollo-router = "1.55.0-rc.1" {{/if}} {{/if}} async-trait = "0.1.52" diff --git a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml index 46d2c1ede9..47b4ad2023 100644 --- a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml @@ -13,7 +13,7 @@ apollo-router-scaffold = { path ="{{integration_test}}apollo-router-scaffold" } {{#if branch}} apollo-router-scaffold = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} -apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.55.0-rc.0" } +apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.55.0-rc.1" } {{/if}} {{/if}} anyhow = "1.0.58" diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 37c0812ab1..f3f63aa195 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router" -version = "1.55.0-rc.0" +version = "1.55.0-rc.1" authors = ["Apollo Graph, Inc. "] repository = "https://github.com/apollographql/router/" documentation = "https://docs.rs/apollo-router" @@ -68,7 +68,7 @@ askama = "0.12.1" access-json = "0.1.0" anyhow = "1.0.86" apollo-compiler.workspace = true -apollo-federation = { path = "../apollo-federation", version = "=1.55.0-rc.0" } +apollo-federation = { path = "../apollo-federation", version = "=1.55.0-rc.1" } arc-swap = "1.6.0" async-channel = "1.9.0" async-compression = { version = "0.4.6", features = [ diff --git a/dockerfiles/tracing/docker-compose.datadog.yml b/dockerfiles/tracing/docker-compose.datadog.yml index 2523f55e50..48c1b83f01 100644 --- a/dockerfiles/tracing/docker-compose.datadog.yml +++ b/dockerfiles/tracing/docker-compose.datadog.yml @@ -3,7 +3,7 @@ services: apollo-router: container_name: apollo-router - image: ghcr.io/apollographql/router:v1.55.0-rc.0 + image: ghcr.io/apollographql/router:v1.55.0-rc.1 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/datadog.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.jaeger.yml b/dockerfiles/tracing/docker-compose.jaeger.yml index 56537704c9..94d6578d38 100644 --- a/dockerfiles/tracing/docker-compose.jaeger.yml +++ b/dockerfiles/tracing/docker-compose.jaeger.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router #build: ./router - image: ghcr.io/apollographql/router:v1.55.0-rc.0 + image: ghcr.io/apollographql/router:v1.55.0-rc.1 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/jaeger.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml index 7efe56a19d..8973d7bc2c 100644 --- a/dockerfiles/tracing/docker-compose.zipkin.yml +++ b/dockerfiles/tracing/docker-compose.zipkin.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router build: ./router - image: ghcr.io/apollographql/router:v1.55.0-rc.0 + image: ghcr.io/apollographql/router:v1.55.0-rc.1 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/zipkin.router.yaml:/etc/config/configuration.yaml diff --git a/helm/chart/router/Chart.yaml b/helm/chart/router/Chart.yaml index ff312bf296..7789a8e5ed 100644 --- a/helm/chart/router/Chart.yaml +++ b/helm/chart/router/Chart.yaml @@ -20,10 +20,10 @@ type: application # so it matches the shape of our release process and release automation. # By proxy of that decision, this version uses SemVer 2.0.0, though the prefix # of "v" is not included. -version: 1.55.0-rc.0 +version: 1.55.0-rc.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.55.0-rc.0" +appVersion: "v1.55.0-rc.1" diff --git a/helm/chart/router/README.md b/helm/chart/router/README.md index e8821181a6..8afc6cf501 100644 --- a/helm/chart/router/README.md +++ b/helm/chart/router/README.md @@ -2,7 +2,7 @@ [router](https://github.com/apollographql/router) Rust Graph Routing runtime for Apollo Federation -![Version: 1.55.0-rc.0](https://img.shields.io/badge/Version-1.55.0--rc.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.55.0-rc.0](https://img.shields.io/badge/AppVersion-v1.55.0--rc.0-informational?style=flat-square) +![Version: 1.55.0-rc.1](https://img.shields.io/badge/Version-1.55.0--rc.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.55.0-rc.1](https://img.shields.io/badge/AppVersion-v1.55.0--rc.1-informational?style=flat-square) ## Prerequisites @@ -11,7 +11,7 @@ ## Get Repo Info ```console -helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0-rc.0 +helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0-rc.1 ``` ## Install Chart @@ -19,7 +19,7 @@ helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0-rc.0 **Important:** only helm3 is supported ```console -helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0-rc.0 --values my-values.yaml +helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0-rc.1 --values my-values.yaml ``` _See [configuration](#configuration) below._ diff --git a/licenses.html b/licenses.html index 8a7c115dfd..ccae66e4b5 100644 --- a/licenses.html +++ b/licenses.html @@ -48,9 +48,9 @@

    Overview of licenses:

  • MIT License (155)
  • BSD 3-Clause "New" or "Revised" License (11)
  • ISC License (8)
  • -
  • Elastic License 2.0 (6)
  • Mozilla Public License 2.0 (5)
  • BSD 2-Clause "Simplified" License (4)
  • +
  • Elastic License 2.0 (3)
  • Creative Commons Zero v1.0 Universal (2)
  • OpenSSL License (1)
  • Unicode License Agreement - Data Files and Software (2016) (1)
  • @@ -12267,9 +12267,6 @@

    Elastic License 2.0

    Used by:

    Copyright 2021 Apollo Graph, Inc.
     
    diff --git a/scripts/install.sh b/scripts/install.sh
    index a3637725a9..9cad7068a4 100755
    --- a/scripts/install.sh
    +++ b/scripts/install.sh
    @@ -11,7 +11,7 @@ BINARY_DOWNLOAD_PREFIX="https://github.com/apollographql/router/releases/downloa
     
     # Router version defined in apollo-router's Cargo.toml
     # Note: Change this line manually during the release steps.
    -PACKAGE_VERSION="v1.55.0-rc.0"
    +PACKAGE_VERSION="v1.55.0-rc.1"
     
     download_binary() {
         downloader --check
    
    From 953fe93a3963183496e42619709eb2252cefe73a Mon Sep 17 00:00:00 2001
    From: Jesse Rosenberger 
    Date: Thu, 19 Sep 2024 21:30:37 +0300
    Subject: [PATCH 38/56] docs: Proactively update doc parts of the Fed v2.9.1
     bump.
    
    Saves time during releasing if you can get out ahead of this.
    
    Ref: 941866363c1e612415e38b4866cd6f60540f3127
    Ref: #6029
    ---
     .changesets/chore_update_router_bridge.md  |  5 ++++-
     docs/source/federation-version-support.mdx | 10 +++++++++-
     2 files changed, 13 insertions(+), 2 deletions(-)
    
    diff --git a/.changesets/chore_update_router_bridge.md b/.changesets/chore_update_router_bridge.md
    index 0505eb1889..33254c6906 100644
    --- a/.changesets/chore_update_router_bridge.md
    +++ b/.changesets/chore_update_router_bridge.md
    @@ -1,5 +1,8 @@
    +> [!IMPORTANT]
    +> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), this release changes the hashing algorithm used for the cache keys.  On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new hashing algorithm comes into service.
    +
     ### Update router-bridge@0.6.2+v2.9.1 ([PR #6027](https://github.com/apollographql/router/pull/6027))
     
     Updates to latest router-bridge and federation version. This federation version fixes edge cases for subgraph extraction logic when using spec renaming or specs URLs that look similar to `specs.apollo.dev`
     
    -By [@lrlna](https://github.com/lrlna) in https://github.com/apollographql/router/pull/6027
    \ No newline at end of file
    +By [@lrlna](https://github.com/lrlna) in https://github.com/apollographql/router/pull/6027
    diff --git a/docs/source/federation-version-support.mdx b/docs/source/federation-version-support.mdx
    index 331b9eb7d2..80ff912745 100644
    --- a/docs/source/federation-version-support.mdx
    +++ b/docs/source/federation-version-support.mdx
    @@ -37,7 +37,15 @@ The table below shows which version of federation each router release is compile
         
         
             
    -            v1.53.0 and later (see latest releases)
    +            v1.55.0 and later (see latest releases)
    +        
    +        
    +            2.9.1
    +        
    +    
    +    
    +        
    +            v1.52.1 - v1.53.0
             
             
                 2.9.0
    
    From 33739e3a67de10b566ece8d1b9f3459a597c7460 Mon Sep 17 00:00:00 2001
    From: Geoffroy Couprie 
    Date: Mon, 23 Sep 2024 09:53:33 +0200
    Subject: [PATCH 39/56] Apply suggestions from code review
    
    Co-authored-by: Edward Huang 
    ---
     .changesets/feat_geal_implement_redis_connection_pooling.md     | 2 +-
     .../fix_geal_remove_unused_fragments_from_filtered_queries.md   | 2 +-
     2 files changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/.changesets/feat_geal_implement_redis_connection_pooling.md b/.changesets/feat_geal_implement_redis_connection_pooling.md
    index ae0dff61d1..00de939ac5 100644
    --- a/.changesets/feat_geal_implement_redis_connection_pooling.md
    +++ b/.changesets/feat_geal_implement_redis_connection_pooling.md
    @@ -1,5 +1,5 @@
     ### Support Redis connection pooling ([PR #5942](https://github.com/apollographql/router/pull/5942))
     
    -This implements Redis connection pooling, for APQ, query planner and entity cache Redis usage. This can improve performance when there is some contention on the Redis connection, or some latency in Redis calls.
    +The router now supports Redis connection pooling for APQs, query planners and entity caches. This can improve performance when there is contention on Redis connections or latency in Redis calls.
     
     By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/5942
    \ No newline at end of file
    diff --git a/.changesets/fix_geal_remove_unused_fragments_from_filtered_queries.md b/.changesets/fix_geal_remove_unused_fragments_from_filtered_queries.md
    index 772675e6b3..d7e2fa91a8 100644
    --- a/.changesets/fix_geal_remove_unused_fragments_from_filtered_queries.md
    +++ b/.changesets/fix_geal_remove_unused_fragments_from_filtered_queries.md
    @@ -1,5 +1,5 @@
     ### Create valid documents with authorization filtering ([PR #5952](https://github.com/apollographql/router/pull/5952))
     
    -This fixes the authorization plugin's query filtering to remove unused fragments and input arguments if the related parts of the query are removed. This was generating validation errors when the query went into query planning
    +This release fixes the authorization plugin's query filtering to remove unused fragments and input arguments if the related parts of the query are removed. Previously the plugin's query filtering generated validation errors when planning the query.
     
     By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/5952
    \ No newline at end of file
    
    From cd9cc25ab608a282bbd9a16d24b6c2da59e24596 Mon Sep 17 00:00:00 2001
    From: Jesse Rosenberger 
    Date: Mon, 23 Sep 2024 13:12:16 +0300
    Subject: [PATCH 40/56] Update .changesets/chore_update_router_bridge.md
    
    Co-authored-by: Edward Huang 
    ---
     .changesets/chore_update_router_bridge.md | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/.changesets/chore_update_router_bridge.md b/.changesets/chore_update_router_bridge.md
    index 33254c6906..bc23827874 100644
    --- a/.changesets/chore_update_router_bridge.md
    +++ b/.changesets/chore_update_router_bridge.md
    @@ -1,8 +1,8 @@
     > [!IMPORTANT]
     > If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), this release changes the hashing algorithm used for the cache keys.  On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new hashing algorithm comes into service.
     
    -### Update router-bridge@0.6.2+v2.9.1 ([PR #6027](https://github.com/apollographql/router/pull/6027))
    +### Update to Federation v2.9.1 ([PR #6029](https://github.com/apollographql/router/pull/6029))
     
    -Updates to latest router-bridge and federation version. This federation version fixes edge cases for subgraph extraction logic when using spec renaming or specs URLs that look similar to `specs.apollo.dev`
    +This release updates to Federation v2.9.1, which fixes edge cases in subgraph extraction logic when using spec renaming or spec URLs (e.g., `specs.apollo.dev`) that could impact the planner's ability to plan a query.
     
     By [@lrlna](https://github.com/lrlna) in https://github.com/apollographql/router/pull/6027
    
    From fecea4b3429c0280e1727c29348094672775a29a Mon Sep 17 00:00:00 2001
    From: Jesse Rosenberger 
    Date: Mon, 23 Sep 2024 13:13:28 +0300
    Subject: [PATCH 41/56] Update .changesets/fix_bryn_fix_gauges.md
    
    Co-authored-by: Edward Huang 
    ---
     .changesets/fix_bryn_fix_gauges.md | 9 ++++-----
     1 file changed, 4 insertions(+), 5 deletions(-)
    
    diff --git a/.changesets/fix_bryn_fix_gauges.md b/.changesets/fix_bryn_fix_gauges.md
    index 0a33f8544f..e17a35c3ea 100644
    --- a/.changesets/fix_bryn_fix_gauges.md
    +++ b/.changesets/fix_bryn_fix_gauges.md
    @@ -1,14 +1,13 @@
    -### Gauges stop working after hot reload ([PR #5996](https://github.com/apollographql/router/pull/5996), [PR #5999](https://github.com/apollographql/router/pull/5999), [PR #5999](https://github.com/apollographql/router/pull/6012))
    +### Fix stopped gauges upon hot reload ([PR #5996](https://github.com/apollographql/router/pull/5996), [PR #5999](https://github.com/apollographql/router/pull/5999), [PR #5999](https://github.com/apollographql/router/pull/6012))
    +
    +Previously when the router hot-reloaded a schema or a configuration file, the following gauges stopped working:
     
    -When the router reloads the schema or config, some gauges stopped working. These were:
     * `apollo.router.cache.storage.estimated_size`
     * `apollo_router_cache_size`
     * `apollo.router.v8.heap.used`
     * `apollo.router.v8.heap.total`
     * `apollo.router.query_planning.queued`
     
    -The gauges will now continue to function after a router hot reload. 
    -
    -As a result of this change, introspection queries will now share the same cache even when query planner pooling is used.
    +This issue has been fixed in this release, and the gauges now continue to function after a router hot-reloads. 
     
     By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5996 and https://github.com/apollographql/router/pull/5999 and https://github.com/apollographql/router/pull/6012
    
    From c66798f3c81acaab8eb0b1c613702ec58c1a69b0 Mon Sep 17 00:00:00 2001
    From: Jesse Rosenberger 
    Date: Mon, 23 Sep 2024 13:17:45 +0300
    Subject: [PATCH 42/56] Update .changesets/exp_qp_cache_prewarm_on_reload.md
    
    Co-authored-by: Edward Huang 
    ---
     .changesets/exp_qp_cache_prewarm_on_reload.md | 12 ++++++++----
     1 file changed, 8 insertions(+), 4 deletions(-)
    
    diff --git a/.changesets/exp_qp_cache_prewarm_on_reload.md b/.changesets/exp_qp_cache_prewarm_on_reload.md
    index 56f87a70e7..59de502083 100644
    --- a/.changesets/exp_qp_cache_prewarm_on_reload.md
    +++ b/.changesets/exp_qp_cache_prewarm_on_reload.md
    @@ -1,12 +1,16 @@
     ### Allow disabling persisted-queries-based query plan cache prewarm on schema reload
     
    -In Router v1.31.0, we started including operations from persisted query lists when Router pre-warms the query plan cache when loading a new schema.
    +The router supports the new `persisted_queries.experimental_prewarm_query_plan_cache.on_reload` configuration option. It toggles whether a query plan cache that's prewarmed upon loading a new schema includes operations from persisted query lists. Its default is `true`. Setting it `false` precludes operations from persisted query lists from being added to the prewarmed query plan cache.
     
    -In Router v1.49.0, we let you also pre-warm the query plan cache from the persisted query list during Router startup by setting `persisted_queries.experimental_prewarm_query_plan_cache` to true.
    +Some background about the development of this option:
     
    -We now allow you to disable the original feature, so that Router will only pre-warm recent operations from the query planning cache when loading a new schema (and not the persisted query list as well), by setting `persisted_queries.experimental_prewarm_query_plan_cache.on_reload` to `false`.
    +- In router v1.31.0, we started including operations from persisted query lists when the router prewarms the query plan cache when loading a new schema.
     
    -The option added in v1.49.0 has been renamed from `persisted_queries.experimental_prewarm_query_plan_cache` to `persisted_queries.experimental_prewarm_query_plan_cache.on_startup`. Existing configuration files will keep working as before, but with a warning that can be resolved by updating your config file:
    +- Then in router v1.49.0, we let you also prewarm the query plan cache from the persisted query list during router startup by setting `persisted_queries.experimental_prewarm_query_plan_cache` to true.
    +
    +- In this release, we now allow you to disable the original feature so that the router can prewarm only recent operations from the query planning cache (and not operations from persisted query lists) when loading a new schema.
    +
    +Note: the option added in v1.49.0 has been renamed from `persisted_queries.experimental_prewarm_query_plan_cache` to `persisted_queries.experimental_prewarm_query_plan_cache.on_startup`. Existing configuration files will keep working as before, but with a warning that can be resolved by updating your config file:
     
     ```diff
      persisted_queries:
    
    From d73755156b179ac733a1ef962759ce4847b76526 Mon Sep 17 00:00:00 2001
    From: Jesse Rosenberger 
    Date: Mon, 23 Sep 2024 13:18:08 +0300
    Subject: [PATCH 43/56] Update
     .changesets/fix_tninesling_demand_control_variable_check.md
    
    Co-authored-by: Edward Huang 
    ---
     .changesets/fix_tninesling_demand_control_variable_check.md | 3 ++-
     1 file changed, 2 insertions(+), 1 deletion(-)
    
    diff --git a/.changesets/fix_tninesling_demand_control_variable_check.md b/.changesets/fix_tninesling_demand_control_variable_check.md
    index 96d7f81270..b1e6d15d2f 100644
    --- a/.changesets/fix_tninesling_demand_control_variable_check.md
    +++ b/.changesets/fix_tninesling_demand_control_variable_check.md
    @@ -1,5 +1,6 @@
     ### Include request variables in demand control scoring ([PR #5995](https://github.com/apollographql/router/pull/5995))
     
    -Fix demand control scoring for queries which use variables.
    +Demand control scoring in the router now accounts for variables in queries.
    +
     
     By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/5995
    
    From e622040a1a40c8d5159c3c9e59b037f7b43711db Mon Sep 17 00:00:00 2001
    From: Jesse Rosenberger 
    Date: Mon, 23 Sep 2024 14:29:26 +0300
    Subject: [PATCH 44/56] Update
     .changesets/feat_tninesling_coprocessor_cost_access.md
    
    Co-authored-by: Edward Huang 
    ---
     .changesets/feat_tninesling_coprocessor_cost_access.md | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/.changesets/feat_tninesling_coprocessor_cost_access.md b/.changesets/feat_tninesling_coprocessor_cost_access.md
    index a8c1a7a6f5..a1502cd788 100644
    --- a/.changesets/feat_tninesling_coprocessor_cost_access.md
    +++ b/.changesets/feat_tninesling_coprocessor_cost_access.md
    @@ -1,5 +1,5 @@
    -### Move cost context values into DashMap ([PR #5972](https://github.com/apollographql/router/pull/5972))
    +### Enable router customizations to access demand control info ([PR #5972](https://github.com/apollographql/router/pull/5972))
     
    -Allow Rhai scripts and coprocessors to access demand control information via context.
    +Rhai scripts and coprocessors can now access demand control information via the context. For more information on Rhai constants to access demand control info, see [available Rhai API constants](https://apollographql.com/docs/router/customizations/rhai-api#available-constants).
     
     By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/5972
    
    From 8c6d7f7f662e5d461bf33169144af7b648e6ec44 Mon Sep 17 00:00:00 2001
    From: Jesse Rosenberger 
    Date: Tue, 24 Sep 2024 13:11:46 +0300
    Subject: [PATCH 45/56] Only run fuzz on the `test` job when `fuzz` is
     requested and skip on releases.
    
    The `fuzz` job uses the nightly version of Rust, necessarily.  However, due
    to the predictability of nightly releases, they can start to fail from time
    to time.  This is particularly problematic on release jobs where it is
    necessary for the jobs to all pass in order to unblock the release.
    
    Therefore, this makes the `fuzz` job disabled by default and only runs it
    when requested and only requests it on non-release jobs (effectively, that
    narrows it down to `ci_checks`).
    ---
     .circleci/config.yml | 9 ++++++++-
     1 file changed, 8 insertions(+), 1 deletion(-)
    
    diff --git a/.circleci/config.yml b/.circleci/config.yml
    index 1d0e41f62c..dad1e8acf6 100644
    --- a/.circleci/config.yml
    +++ b/.circleci/config.yml
    @@ -574,6 +574,9 @@ jobs:
         parameters:
           platform:
             type: executor
    +      fuzz:
    +        type: boolean
    +        default: false
         executor: << parameters.platform >>
         steps:
           - checkout
    @@ -582,7 +585,9 @@ jobs:
           - xtask_test
           - when:
               condition:
    -            equal: [ *arm_linux_test_executor, << parameters.platform >> ]
    +            and:
    +              - equal: [ true, << parameters.fuzz >> ]
    +              - equal: [ *amd_linux_test_executor, << parameters.platform >> ]
               steps:
                 - fuzz_build
     
    @@ -979,6 +984,8 @@ workflows:
                 parameters:
                   platform:
                     [ macos_test, windows_test, amd_linux_test, arm_linux_test ]
    +              fuzz:
    +                [ true ]
     
       quick-nightly:
         when: << pipeline.parameters.quick_nightly >>
    
    From 34184ebc5b997c7905ca8ce63ff4ce4ade17361e Mon Sep 17 00:00:00 2001
    From: Jesse Rosenberger 
    Date: Tue, 24 Sep 2024 13:17:11 +0300
    Subject: [PATCH 46/56] prep release: v1.55.0-rc.2
    
    ---
     Cargo.lock                                    |  8 +-
     apollo-federation/Cargo.toml                  |  2 +-
     apollo-router-benchmarks/Cargo.toml           |  2 +-
     apollo-router-scaffold/Cargo.toml             |  2 +-
     .../templates/base/Cargo.template.toml        |  2 +-
     .../templates/base/xtask/Cargo.template.toml  |  2 +-
     apollo-router/Cargo.toml                      |  4 +-
     .../tracing/docker-compose.datadog.yml        |  2 +-
     dockerfiles/tracing/docker-compose.jaeger.yml |  2 +-
     dockerfiles/tracing/docker-compose.zipkin.yml |  2 +-
     helm/chart/router/Chart.yaml                  |  4 +-
     helm/chart/router/README.md                   |  6 +-
     licenses.html                                 | 97 ++-----------------
     scripts/install.sh                            |  2 +-
     14 files changed, 26 insertions(+), 111 deletions(-)
    
    diff --git a/Cargo.lock b/Cargo.lock
    index d801edc2fc..fc611a1cc9 100644
    --- a/Cargo.lock
    +++ b/Cargo.lock
    @@ -178,7 +178,7 @@ dependencies = [
     
     [[package]]
     name = "apollo-federation"
    -version = "1.55.0-rc.1"
    +version = "1.55.0-rc.2"
     dependencies = [
      "apollo-compiler",
      "derive_more",
    @@ -229,7 +229,7 @@ dependencies = [
     
     [[package]]
     name = "apollo-router"
    -version = "1.55.0-rc.1"
    +version = "1.55.0-rc.2"
     dependencies = [
      "access-json",
      "ahash",
    @@ -400,7 +400,7 @@ dependencies = [
     
     [[package]]
     name = "apollo-router-benchmarks"
    -version = "1.55.0-rc.1"
    +version = "1.55.0-rc.2"
     dependencies = [
      "apollo-parser",
      "apollo-router",
    @@ -416,7 +416,7 @@ dependencies = [
     
     [[package]]
     name = "apollo-router-scaffold"
    -version = "1.55.0-rc.1"
    +version = "1.55.0-rc.2"
     dependencies = [
      "anyhow",
      "cargo-scaffold",
    diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml
    index da555587f4..504c350ee8 100644
    --- a/apollo-federation/Cargo.toml
    +++ b/apollo-federation/Cargo.toml
    @@ -1,6 +1,6 @@
     [package]
     name = "apollo-federation"
    -version = "1.55.0-rc.1"
    +version = "1.55.0-rc.2"
     authors = ["The Apollo GraphQL Contributors"]
     edition = "2021"
     description = "Apollo Federation"
    diff --git a/apollo-router-benchmarks/Cargo.toml b/apollo-router-benchmarks/Cargo.toml
    index 396c62a76b..523790860d 100644
    --- a/apollo-router-benchmarks/Cargo.toml
    +++ b/apollo-router-benchmarks/Cargo.toml
    @@ -1,6 +1,6 @@
     [package]
     name = "apollo-router-benchmarks"
    -version = "1.55.0-rc.1"
    +version = "1.55.0-rc.2"
     authors = ["Apollo Graph, Inc. "]
     edition = "2021"
     license = "Elastic-2.0"
    diff --git a/apollo-router-scaffold/Cargo.toml b/apollo-router-scaffold/Cargo.toml
    index bbc2f33351..aa0eacd8b0 100644
    --- a/apollo-router-scaffold/Cargo.toml
    +++ b/apollo-router-scaffold/Cargo.toml
    @@ -1,6 +1,6 @@
     [package]
     name = "apollo-router-scaffold"
    -version = "1.55.0-rc.1"
    +version = "1.55.0-rc.2"
     authors = ["Apollo Graph, Inc. "]
     edition = "2021"
     license = "Elastic-2.0"
    diff --git a/apollo-router-scaffold/templates/base/Cargo.template.toml b/apollo-router-scaffold/templates/base/Cargo.template.toml
    index 2a3940cf81..fe7de1521c 100644
    --- a/apollo-router-scaffold/templates/base/Cargo.template.toml
    +++ b/apollo-router-scaffold/templates/base/Cargo.template.toml
    @@ -22,7 +22,7 @@ apollo-router = { path ="{{integration_test}}apollo-router" }
     apollo-router = { git="https://github.com/apollographql/router.git", branch="{{branch}}" }
     {{else}}
     # Note if you update these dependencies then also update xtask/Cargo.toml
    -apollo-router = "1.55.0-rc.1"
    +apollo-router = "1.55.0-rc.2"
     {{/if}}
     {{/if}}
     async-trait = "0.1.52"
    diff --git a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml
    index 47b4ad2023..23a17e421b 100644
    --- a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml
    +++ b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml
    @@ -13,7 +13,7 @@ apollo-router-scaffold = { path ="{{integration_test}}apollo-router-scaffold" }
     {{#if branch}}
     apollo-router-scaffold = { git="https://github.com/apollographql/router.git", branch="{{branch}}" }
     {{else}}
    -apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.55.0-rc.1" }
    +apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.55.0-rc.2" }
     {{/if}}
     {{/if}}
     anyhow = "1.0.58"
    diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml
    index 13e541af00..4f4aa76ce5 100644
    --- a/apollo-router/Cargo.toml
    +++ b/apollo-router/Cargo.toml
    @@ -1,6 +1,6 @@
     [package]
     name = "apollo-router"
    -version = "1.55.0-rc.1"
    +version = "1.55.0-rc.2"
     authors = ["Apollo Graph, Inc. "]
     repository = "https://github.com/apollographql/router/"
     documentation = "https://docs.rs/apollo-router"
    @@ -68,7 +68,7 @@ askama = "0.12.1"
     access-json = "0.1.0"
     anyhow = "1.0.86"
     apollo-compiler.workspace = true
    -apollo-federation = { path = "../apollo-federation", version = "=1.55.0-rc.1" }
    +apollo-federation = { path = "../apollo-federation", version = "=1.55.0-rc.2" }
     arc-swap = "1.6.0"
     async-channel = "1.9.0"
     async-compression = { version = "0.4.6", features = [
    diff --git a/dockerfiles/tracing/docker-compose.datadog.yml b/dockerfiles/tracing/docker-compose.datadog.yml
    index 48c1b83f01..e59725accb 100644
    --- a/dockerfiles/tracing/docker-compose.datadog.yml
    +++ b/dockerfiles/tracing/docker-compose.datadog.yml
    @@ -3,7 +3,7 @@ services:
     
       apollo-router:
         container_name: apollo-router
    -    image: ghcr.io/apollographql/router:v1.55.0-rc.1
    +    image: ghcr.io/apollographql/router:v1.55.0-rc.2
         volumes:
           - ./supergraph.graphql:/etc/config/supergraph.graphql
           - ./router/datadog.router.yaml:/etc/config/configuration.yaml
    diff --git a/dockerfiles/tracing/docker-compose.jaeger.yml b/dockerfiles/tracing/docker-compose.jaeger.yml
    index 94d6578d38..ac96c102fe 100644
    --- a/dockerfiles/tracing/docker-compose.jaeger.yml
    +++ b/dockerfiles/tracing/docker-compose.jaeger.yml
    @@ -4,7 +4,7 @@ services:
       apollo-router:
         container_name: apollo-router
         #build: ./router
    -    image: ghcr.io/apollographql/router:v1.55.0-rc.1
    +    image: ghcr.io/apollographql/router:v1.55.0-rc.2
         volumes:
           - ./supergraph.graphql:/etc/config/supergraph.graphql
           - ./router/jaeger.router.yaml:/etc/config/configuration.yaml
    diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml
    index 8973d7bc2c..a3f2844a49 100644
    --- a/dockerfiles/tracing/docker-compose.zipkin.yml
    +++ b/dockerfiles/tracing/docker-compose.zipkin.yml
    @@ -4,7 +4,7 @@ services:
       apollo-router:
         container_name: apollo-router
         build: ./router
    -    image: ghcr.io/apollographql/router:v1.55.0-rc.1
    +    image: ghcr.io/apollographql/router:v1.55.0-rc.2
         volumes:
           - ./supergraph.graphql:/etc/config/supergraph.graphql
           - ./router/zipkin.router.yaml:/etc/config/configuration.yaml
    diff --git a/helm/chart/router/Chart.yaml b/helm/chart/router/Chart.yaml
    index 7789a8e5ed..4e1453f903 100644
    --- a/helm/chart/router/Chart.yaml
    +++ b/helm/chart/router/Chart.yaml
    @@ -20,10 +20,10 @@ type: application
     # so it matches the shape of our release process and release automation.
     # By proxy of that decision, this version uses SemVer 2.0.0, though the prefix
     # of "v" is not included.
    -version: 1.55.0-rc.1
    +version: 1.55.0-rc.2
     
     # This is the version number of the application being deployed. This version number should be
     # incremented each time you make changes to the application. Versions are not expected to
     # follow Semantic Versioning. They should reflect the version the application is using.
     # It is recommended to use it with quotes.
    -appVersion: "v1.55.0-rc.1"
    +appVersion: "v1.55.0-rc.2"
    diff --git a/helm/chart/router/README.md b/helm/chart/router/README.md
    index 8afc6cf501..5b3abde642 100644
    --- a/helm/chart/router/README.md
    +++ b/helm/chart/router/README.md
    @@ -2,7 +2,7 @@
     
     [router](https://github.com/apollographql/router) Rust Graph Routing runtime for Apollo Federation
     
    -![Version: 1.55.0-rc.1](https://img.shields.io/badge/Version-1.55.0--rc.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.55.0-rc.1](https://img.shields.io/badge/AppVersion-v1.55.0--rc.1-informational?style=flat-square)
    +![Version: 1.55.0-rc.2](https://img.shields.io/badge/Version-1.55.0--rc.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.55.0-rc.2](https://img.shields.io/badge/AppVersion-v1.55.0--rc.2-informational?style=flat-square)
     
     ## Prerequisites
     
    @@ -11,7 +11,7 @@
     ## Get Repo Info
     
     ```console
    -helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0-rc.1
    +helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0-rc.2
     ```
     
     ## Install Chart
    @@ -19,7 +19,7 @@ helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0-rc.1
     **Important:** only helm3 is supported
     
     ```console
    -helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0-rc.1 --values my-values.yaml
    +helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0-rc.2 --values my-values.yaml
     ```
     
     _See [configuration](#configuration) below._
    diff --git a/licenses.html b/licenses.html
    index ccae66e4b5..30647c049e 100644
    --- a/licenses.html
    +++ b/licenses.html
    @@ -44,8 +44,8 @@ 

    Third Party Licenses

    Overview of licenses:

    - -
  • -

    Apache License 2.0

    -

    Used by:

    - -
    Copyright [2022] [Bryn Cooke]
    -
    -Licensed under the Apache License, Version 2.0 (the "License");
    -you may not use this file except in compliance with the License.
    -You may obtain a copy of the License at
    -
    -    http://www.apache.org/licenses/LICENSE-2.0
    -
     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    @@ -13322,66 +13299,6 @@ 

    Used by:

    shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -
    -
  • -
  • -

    MIT License

    -

    Used by:

    - -
    Copyright (c) 2019 Carl Lerche
    -
    -Permission is hereby granted, free of charge, to any
    -person obtaining a copy of this software and associated
    -documentation files (the "Software"), to deal in the
    -Software without restriction, including without
    -limitation the rights to use, copy, modify, merge,
    -publish, distribute, sublicense, and/or sell copies of
    -the Software, and to permit persons to whom the Software
    -is furnished to do so, subject to the following
    -conditions:
    -
    -The above copyright notice and this permission notice
    -shall be included in all copies or substantial portions
    -of the Software.
    -
    -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
    -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
    -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
    -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
    -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    -DEALINGS IN THE SOFTWARE.
    -
    -Copyright (c) 2018 David Tolnay
    -
    -Permission is hereby granted, free of charge, to any
    -person obtaining a copy of this software and associated
    -documentation files (the "Software"), to deal in the
    -Software without restriction, including without
    -limitation the rights to use, copy, modify, merge,
    -publish, distribute, sublicense, and/or sell copies of
    -the Software, and to permit persons to whom the Software
    -is furnished to do so, subject to the following
    -conditions:
    -
    -The above copyright notice and this permission notice
    -shall be included in all copies or substantial portions
    -of the Software.
    -
     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
     ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
     TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    @@ -14388,6 +14305,8 @@ 

    Used by:

    MIT License

    Used by:

      +
    • async-stream
    • +
    • async-stream-impl
    • base64-simd
    • convert_case
    • cookie-factory
    • @@ -14730,8 +14649,6 @@

      Used by:

      The MIT License (MIT)
       
      @@ -15260,8 +15177,6 @@ 

      Used by:

      MIT License

      Used by:

        -
      • aho-corasick
      • -
      • byteorder
      • globset
      • memchr
      • regex-automata
      • @@ -15669,6 +15584,7 @@

        Used by:

        Mozilla Public License 2.0

        Used by:

        Mozilla Public License Version 2.0
        @@ -16051,7 +15967,6 @@ 

        Used by:

        Mozilla Public License 2.0

        Used by:

        Mozilla Public License Version 2.0
        diff --git a/scripts/install.sh b/scripts/install.sh
        index 9cad7068a4..1dca7b24db 100755
        --- a/scripts/install.sh
        +++ b/scripts/install.sh
        @@ -11,7 +11,7 @@ BINARY_DOWNLOAD_PREFIX="https://github.com/apollographql/router/releases/downloa
         
         # Router version defined in apollo-router's Cargo.toml
         # Note: Change this line manually during the release steps.
        -PACKAGE_VERSION="v1.55.0-rc.1"
        +PACKAGE_VERSION="v1.55.0-rc.2"
         
         download_binary() {
             downloader --check
        
        From d232d11d487e284f6820dad4c45d4a6eca0413fb Mon Sep 17 00:00:00 2001
        From: Jesse Rosenberger 
        Date: Tue, 24 Sep 2024 14:20:25 +0300
        Subject: [PATCH 47/56] Follow-up on fuzz-constraining workflow with
         matrix-avoiding change.
        
        ---
         .circleci/config.yml | 3 +--
         1 file changed, 1 insertion(+), 2 deletions(-)
        
        diff --git a/.circleci/config.yml b/.circleci/config.yml
        index dad1e8acf6..6129994fac 100644
        --- a/.circleci/config.yml
        +++ b/.circleci/config.yml
        @@ -976,6 +976,7 @@ workflows:
                       platform:
                         [ amd_linux_test ]
               - test:
        +          fuzz: true
                   requires:
                     - lint
                     - check_helm
        @@ -984,8 +985,6 @@ workflows:
                     parameters:
                       platform:
                         [ macos_test, windows_test, amd_linux_test, arm_linux_test ]
        -              fuzz:
        -                [ true ]
         
           quick-nightly:
             when: << pipeline.parameters.quick_nightly >>
        
        From f4562fd27128573369fd0afed987966355402804 Mon Sep 17 00:00:00 2001
        From: Jesse Rosenberger 
        Date: Tue, 24 Sep 2024 15:03:15 +0300
        Subject: [PATCH 48/56] fix: Correctly guard FOR `arm` rather than `amd` in
         fuzz-run gate
        
        This corrects a bug that was introduced by auto-complete gone wrong! Fuzzing
        is only meant to run on ARM
        ---
         .circleci/config.yml | 2 +-
         1 file changed, 1 insertion(+), 1 deletion(-)
        
        diff --git a/.circleci/config.yml b/.circleci/config.yml
        index 6129994fac..5330bfc8e0 100644
        --- a/.circleci/config.yml
        +++ b/.circleci/config.yml
        @@ -587,7 +587,7 @@ jobs:
                   condition:
                     and:
                       - equal: [ true, << parameters.fuzz >> ]
        -              - equal: [ *amd_linux_test_executor, << parameters.platform >> ]
        +              - equal: [ *arm_linux_test_executor, << parameters.platform >> ]
                   steps:
                     - fuzz_build
         
        
        From 00e241d9d7f7afb0540b8515e6a47fac5ef5713c Mon Sep 17 00:00:00 2001
        From: Jesse Rosenberger 
        Date: Tue, 24 Sep 2024 15:30:05 +0300
        Subject: [PATCH 49/56] Apply suggestions from code review
        
        Co-authored-by: Edward Huang 
        ---
         .changesets/config_introspection_both.md         | 16 ++++++++++++----
         .changesets/feat_bnjjj_fix_5930.md               | 10 ++++++----
         .../fix_bryn_datadog_sample_propagation.md       |  6 +++---
         3 files changed, 21 insertions(+), 11 deletions(-)
        
        diff --git a/.changesets/config_introspection_both.md b/.changesets/config_introspection_both.md
        index 036cdfe83f..4e51abf53d 100644
        --- a/.changesets/config_introspection_both.md
        +++ b/.changesets/config_introspection_both.md
        @@ -1,10 +1,18 @@
        -### Enable both introspection implementation by default ([PR #6014](https://github.com/apollographql/router/pull/6014))
        +### Enable new and old schema introspection implementations by default ([PR #6014](https://github.com/apollographql/router/pull/6014))
         
        -As part of the process to replace JavaScript schema introspection with a more performant Rust implementation in the router, we are enabling the router to run both implementations as a default. This allows us to definitively assess reliability and stability of Rust implementation before completely removing JavaScript one. As before, it's possible to toggle between implementations using the `experimental_introspection_mode` config key. Possible values are: `new` (runs only Rust-based validation), `legacy` (runs only JS-based validation), `both` (runs both in comparison, logging errors if a difference arises).
        +Starting with this release, if schema introspection is enabled, the router runs both the old Javascript implementation and a new Rust implementation of its introspection logic by default. 
         
        -The `both` mode is now the default, which will result in **no client-facing impact** but will record the metrics for the outcome of comparison as a `apollo.router.operations.introspection.both` counter. If this counter in your metrics has `rust_error = true` or `is_matched = false`, please open an issue.
        +The more performant Rust implementation will eventually replace the Javascript implementation. For now, both implementations are run by default so we can definitively assess the reliability and stability of the Rust implementation before removing the Javascript one. 
         
        -Schema introspection itself is disabled by default, so the above has no effect unless it is enabled in configuration:
        +You can still toggle between implementations using the `experimental_introspection_mode` configuration key. Its valid values: 
        +
        +- `new` runs only Rust-based validation
        +- `legacy` runs only Javascript-based validation
        +- `both` (default) runs both in comparison and logs errors if differences arise
        +
        +Having `both` as the default causes no client-facing impact. It will record and output the metrics of its comparison as a `apollo.router.operations.introspection.both` counter. (Note: if this counter in your metrics has `rust_error = true` or `is_matched = false`, please open an issue with Apollo.)
        +
        +Note: schema introspection itself is disabled by default, so its implementation(s) are run only if it's enabled in your configuration:
         
         ```yaml
         supergraph:
        diff --git a/.changesets/feat_bnjjj_fix_5930.md b/.changesets/feat_bnjjj_fix_5930.md
        index f8c2ac78f6..3d2340fe25 100644
        --- a/.changesets/feat_bnjjj_fix_5930.md
        +++ b/.changesets/feat_bnjjj_fix_5930.md
        @@ -1,8 +1,10 @@
        -### Add ability to alias standard attributes for telemetry ([Issue #5930](https://github.com/apollographql/router/issues/5930))
        +### Support aliasing standard attributes for telemetry ([Issue #5930](https://github.com/apollographql/router/issues/5930))
         
        -There is an issue when using standard attributes (on cache for example) because on new relic `entity.type` is a reserved attribute name and so it won’t work properly. cf [Learn about New Relic entities](https://docs.newrelic.com/docs/new-relic-solutions/new-relic-one/core-concepts/what-entity-new-relic/#reserved-attributes)  Moreover `entity.type` is not consistent with our other graphql attributes (prefixed by `graphql.`). So we rename `entity.type` attribute to `graphql.type.name`.
        +The router now supports creating aliases for standard attributes for telemetry.
         
        -In order to make it work and that could also answer other use cases that would be great if we can alias the name of a standard attribute like this:
        +This fixes issues where standard attribute names collide with reserved attribute names. For example, the standard attribute name `entity.type` is a [reserved attribute](New Relic entities](https://docs.newrelic.com/docs/new-relic-solutions/new-relic-one/core-concepts/what-entity-new-relic/#reserved-attributes) name for New Relic, so it won't work properly. Moreover `entity.type` is inconsistent with our other GraphQL attributes prefixed with `graphql.` 
        +
        +The example configuration below renames `entity.type` to `graphql.type.name`:
         
         ```yaml
         telemetry:
        @@ -13,7 +15,7 @@ telemetry:
               cache: # Cache instruments configuration
                 apollo.router.operations.entity.cache: # A counter which counts the number of cache hit and miss for subgraph requests
                   attributes:
        -            graphql.type.name:
        +            graphql.type.name: # renames entity.type
                       alias: entity_type # ENABLED and aliased to entity_type
         ```
         
        diff --git a/.changesets/fix_bryn_datadog_sample_propagation.md b/.changesets/fix_bryn_datadog_sample_propagation.md
        index 87b2322d51..d5952b4ddf 100644
        --- a/.changesets/fix_bryn_datadog_sample_propagation.md
        +++ b/.changesets/fix_bryn_datadog_sample_propagation.md
        @@ -1,7 +1,7 @@
        -### Fix datadog sample propagation ([PR #6005](https://github.com/apollographql/router/pull/6005))
        +### Fix Datadog sample propagation ([PR #6005](https://github.com/apollographql/router/pull/6005))
         
        -#5788 introduced a regression where samping was being set on propagated headers regardless of the sampling decision in the router or upstream.
        +[PR #5788](https://github.com/apollographql/router/pull/5788) introduced a regression where sampling was set on propagated headers regardless of the sampling decision in the router or upstream.
         
        -This PR reverts the code in question and adds a test to check that a non-sampled request will not result in sampling in the downstream subgraph service.
        +This PR reverts the code in question and adds a test to check that a non-sampled request doesn't result in sampling in the downstream subgraph service.
         
         By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/6005
        
        From 9bc46cad28e036e0843ee69288fadc24eec0b206 Mon Sep 17 00:00:00 2001
        From: Jesse Rosenberger 
        Date: Tue, 24 Sep 2024 17:29:11 +0300
        Subject: [PATCH 50/56] Do the release without fuzz-testing enabled.
        
        ---
         .circleci/config.yml | 3 ++-
         1 file changed, 2 insertions(+), 1 deletion(-)
        
        diff --git a/.circleci/config.yml b/.circleci/config.yml
        index 5330bfc8e0..6c4daf0a3d 100644
        --- a/.circleci/config.yml
        +++ b/.circleci/config.yml
        @@ -976,7 +976,8 @@ workflows:
                       platform:
                         [ amd_linux_test ]
               - test:
        -          fuzz: true
        +          # this should be changed back to true on dev after release
        +          fuzz: false
                   requires:
                     - lint
                     - check_helm
        
        From ef5043aef5e49828fedfc008f26fa497995af257 Mon Sep 17 00:00:00 2001
        From: Jesse Rosenberger 
        Date: Tue, 24 Sep 2024 17:33:53 +0300
        Subject: [PATCH 51/56] prep release: v1.55.0 (#6047)
        
        ---
         .changesets/config_introspection_both.md      |  22 ---
         .changesets/exp_qp_cache_prewarm_on_reload.md |  24 ---
         .changesets/feat_bnjjj_fix_5930.md            |  22 ---
         ...geal_implement_redis_connection_pooling.md |   5 -
         ...feat_tninesling_coprocessor_cost_access.md |   5 -
         .../fix_bryn_datadog_sample_propagation.md    |   7 -
         .changesets/fix_bryn_fix_gauges.md            |  13 --
         ..._unused_fragments_from_filtered_queries.md |   5 -
         ...ninesling_demand_control_variable_check.md |   6 -
         CHANGELOG.md                                  | 175 +++++++++++++++---
         Cargo.lock                                    |   8 +-
         apollo-federation/Cargo.toml                  |   2 +-
         apollo-router-benchmarks/Cargo.toml           |   2 +-
         apollo-router-scaffold/Cargo.toml             |   2 +-
         .../templates/base/Cargo.template.toml        |   2 +-
         .../templates/base/xtask/Cargo.template.toml  |   2 +-
         apollo-router/Cargo.toml                      |   4 +-
         .../tracing/docker-compose.datadog.yml        |   2 +-
         dockerfiles/tracing/docker-compose.jaeger.yml |   2 +-
         dockerfiles/tracing/docker-compose.zipkin.yml |   2 +-
         helm/chart/router/Chart.yaml                  |   4 +-
         helm/chart/router/README.md                   |   6 +-
         licenses.html                                 | 106 ++++++++++-
         scripts/install.sh                            |   2 +-
         24 files changed, 270 insertions(+), 160 deletions(-)
         delete mode 100644 .changesets/config_introspection_both.md
         delete mode 100644 .changesets/exp_qp_cache_prewarm_on_reload.md
         delete mode 100644 .changesets/feat_bnjjj_fix_5930.md
         delete mode 100644 .changesets/feat_geal_implement_redis_connection_pooling.md
         delete mode 100644 .changesets/feat_tninesling_coprocessor_cost_access.md
         delete mode 100644 .changesets/fix_bryn_datadog_sample_propagation.md
         delete mode 100644 .changesets/fix_bryn_fix_gauges.md
         delete mode 100644 .changesets/fix_geal_remove_unused_fragments_from_filtered_queries.md
         delete mode 100644 .changesets/fix_tninesling_demand_control_variable_check.md
        
        diff --git a/.changesets/config_introspection_both.md b/.changesets/config_introspection_both.md
        deleted file mode 100644
        index 4e51abf53d..0000000000
        --- a/.changesets/config_introspection_both.md
        +++ /dev/null
        @@ -1,22 +0,0 @@
        -### Enable new and old schema introspection implementations by default ([PR #6014](https://github.com/apollographql/router/pull/6014))
        -
        -Starting with this release, if schema introspection is enabled, the router runs both the old Javascript implementation and a new Rust implementation of its introspection logic by default. 
        -
        -The more performant Rust implementation will eventually replace the Javascript implementation. For now, both implementations are run by default so we can definitively assess the reliability and stability of the Rust implementation before removing the Javascript one. 
        -
        -You can still toggle between implementations using the `experimental_introspection_mode` configuration key. Its valid values: 
        -
        -- `new` runs only Rust-based validation
        -- `legacy` runs only Javascript-based validation
        -- `both` (default) runs both in comparison and logs errors if differences arise
        -
        -Having `both` as the default causes no client-facing impact. It will record and output the metrics of its comparison as a `apollo.router.operations.introspection.both` counter. (Note: if this counter in your metrics has `rust_error = true` or `is_matched = false`, please open an issue with Apollo.)
        -
        -Note: schema introspection itself is disabled by default, so its implementation(s) are run only if it's enabled in your configuration:
        -
        -```yaml
        -supergraph:
        -  introspection: true
        -```
        -
        -By [@SimonSapin](https://github.com/SimonSapin) in https://github.com/apollographql/router/pull/6014
        diff --git a/.changesets/exp_qp_cache_prewarm_on_reload.md b/.changesets/exp_qp_cache_prewarm_on_reload.md
        deleted file mode 100644
        index 59de502083..0000000000
        --- a/.changesets/exp_qp_cache_prewarm_on_reload.md
        +++ /dev/null
        @@ -1,24 +0,0 @@
        -### Allow disabling persisted-queries-based query plan cache prewarm on schema reload
        -
        -The router supports the new `persisted_queries.experimental_prewarm_query_plan_cache.on_reload` configuration option. It toggles whether a query plan cache that's prewarmed upon loading a new schema includes operations from persisted query lists. Its default is `true`. Setting it `false` precludes operations from persisted query lists from being added to the prewarmed query plan cache.
        -
        -Some background about the development of this option:
        -
        -- In router v1.31.0, we started including operations from persisted query lists when the router prewarms the query plan cache when loading a new schema.
        -
        -- Then in router v1.49.0, we let you also prewarm the query plan cache from the persisted query list during router startup by setting `persisted_queries.experimental_prewarm_query_plan_cache` to true.
        -
        -- In this release, we now allow you to disable the original feature so that the router can prewarm only recent operations from the query planning cache (and not operations from persisted query lists) when loading a new schema.
        -
        -Note: the option added in v1.49.0 has been renamed from `persisted_queries.experimental_prewarm_query_plan_cache` to `persisted_queries.experimental_prewarm_query_plan_cache.on_startup`. Existing configuration files will keep working as before, but with a warning that can be resolved by updating your config file:
        -
        -```diff
        - persisted_queries:
        -   enabled: true
        --  experimental_prewarm_query_plan_cache: true
        -+  experimental_prewarm_query_plan_cache:
        -+    on_startup: true
        -```
        -
        -
        -By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/5990
        \ No newline at end of file
        diff --git a/.changesets/feat_bnjjj_fix_5930.md b/.changesets/feat_bnjjj_fix_5930.md
        deleted file mode 100644
        index 3d2340fe25..0000000000
        --- a/.changesets/feat_bnjjj_fix_5930.md
        +++ /dev/null
        @@ -1,22 +0,0 @@
        -### Support aliasing standard attributes for telemetry ([Issue #5930](https://github.com/apollographql/router/issues/5930))
        -
        -The router now supports creating aliases for standard attributes for telemetry.
        -
        -This fixes issues where standard attribute names collide with reserved attribute names. For example, the standard attribute name `entity.type` is a [reserved attribute](New Relic entities](https://docs.newrelic.com/docs/new-relic-solutions/new-relic-one/core-concepts/what-entity-new-relic/#reserved-attributes) name for New Relic, so it won't work properly. Moreover `entity.type` is inconsistent with our other GraphQL attributes prefixed with `graphql.` 
        -
        -The example configuration below renames `entity.type` to `graphql.type.name`:
        -
        -```yaml
        -telemetry:
        -  instrumentation:
        -    spans:
        -      mode: spec_compliant # Docs state this significantly improves performance: https://www.apollographql.com/docs/router/configuration/telemetry/instrumentation/spans#spec_compliant
        -    instruments:
        -      cache: # Cache instruments configuration
        -        apollo.router.operations.entity.cache: # A counter which counts the number of cache hit and miss for subgraph requests
        -          attributes:
        -            graphql.type.name: # renames entity.type
        -              alias: entity_type # ENABLED and aliased to entity_type
        -```
        -
        -By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/5957
        diff --git a/.changesets/feat_geal_implement_redis_connection_pooling.md b/.changesets/feat_geal_implement_redis_connection_pooling.md
        deleted file mode 100644
        index 00de939ac5..0000000000
        --- a/.changesets/feat_geal_implement_redis_connection_pooling.md
        +++ /dev/null
        @@ -1,5 +0,0 @@
        -### Support Redis connection pooling ([PR #5942](https://github.com/apollographql/router/pull/5942))
        -
        -The router now supports Redis connection pooling for APQs, query planners and entity caches. This can improve performance when there is contention on Redis connections or latency in Redis calls.
        -
        -By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/5942
        \ No newline at end of file
        diff --git a/.changesets/feat_tninesling_coprocessor_cost_access.md b/.changesets/feat_tninesling_coprocessor_cost_access.md
        deleted file mode 100644
        index a1502cd788..0000000000
        --- a/.changesets/feat_tninesling_coprocessor_cost_access.md
        +++ /dev/null
        @@ -1,5 +0,0 @@
        -### Enable router customizations to access demand control info ([PR #5972](https://github.com/apollographql/router/pull/5972))
        -
        -Rhai scripts and coprocessors can now access demand control information via the context. For more information on Rhai constants to access demand control info, see [available Rhai API constants](https://apollographql.com/docs/router/customizations/rhai-api#available-constants).
        -
        -By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/5972
        diff --git a/.changesets/fix_bryn_datadog_sample_propagation.md b/.changesets/fix_bryn_datadog_sample_propagation.md
        deleted file mode 100644
        index d5952b4ddf..0000000000
        --- a/.changesets/fix_bryn_datadog_sample_propagation.md
        +++ /dev/null
        @@ -1,7 +0,0 @@
        -### Fix Datadog sample propagation ([PR #6005](https://github.com/apollographql/router/pull/6005))
        -
        -[PR #5788](https://github.com/apollographql/router/pull/5788) introduced a regression where sampling was set on propagated headers regardless of the sampling decision in the router or upstream.
        -
        -This PR reverts the code in question and adds a test to check that a non-sampled request doesn't result in sampling in the downstream subgraph service.
        -
        -By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/6005
        diff --git a/.changesets/fix_bryn_fix_gauges.md b/.changesets/fix_bryn_fix_gauges.md
        deleted file mode 100644
        index e17a35c3ea..0000000000
        --- a/.changesets/fix_bryn_fix_gauges.md
        +++ /dev/null
        @@ -1,13 +0,0 @@
        -### Fix stopped gauges upon hot reload ([PR #5996](https://github.com/apollographql/router/pull/5996), [PR #5999](https://github.com/apollographql/router/pull/5999), [PR #5999](https://github.com/apollographql/router/pull/6012))
        -
        -Previously when the router hot-reloaded a schema or a configuration file, the following gauges stopped working:
        -
        -* `apollo.router.cache.storage.estimated_size`
        -* `apollo_router_cache_size`
        -* `apollo.router.v8.heap.used`
        -* `apollo.router.v8.heap.total`
        -* `apollo.router.query_planning.queued`
        -
        -This issue has been fixed in this release, and the gauges now continue to function after a router hot-reloads. 
        -
        -By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5996 and https://github.com/apollographql/router/pull/5999 and https://github.com/apollographql/router/pull/6012
        diff --git a/.changesets/fix_geal_remove_unused_fragments_from_filtered_queries.md b/.changesets/fix_geal_remove_unused_fragments_from_filtered_queries.md
        deleted file mode 100644
        index d7e2fa91a8..0000000000
        --- a/.changesets/fix_geal_remove_unused_fragments_from_filtered_queries.md
        +++ /dev/null
        @@ -1,5 +0,0 @@
        -### Create valid documents with authorization filtering ([PR #5952](https://github.com/apollographql/router/pull/5952))
        -
        -This release fixes the authorization plugin's query filtering to remove unused fragments and input arguments if the related parts of the query are removed. Previously the plugin's query filtering generated validation errors when planning the query.
        -
        -By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/5952
        \ No newline at end of file
        diff --git a/.changesets/fix_tninesling_demand_control_variable_check.md b/.changesets/fix_tninesling_demand_control_variable_check.md
        deleted file mode 100644
        index b1e6d15d2f..0000000000
        --- a/.changesets/fix_tninesling_demand_control_variable_check.md
        +++ /dev/null
        @@ -1,6 +0,0 @@
        -### Include request variables in demand control scoring ([PR #5995](https://github.com/apollographql/router/pull/5995))
        -
        -Demand control scoring in the router now accounts for variables in queries.
        -
        -
        -By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/5995
        diff --git a/CHANGELOG.md b/CHANGELOG.md
        index 98e93b3042..73dc9ffd54 100644
        --- a/CHANGELOG.md
        +++ b/CHANGELOG.md
        @@ -4,6 +4,133 @@ All notable changes to Router will be documented in this file.
         
         This project adheres to [Semantic Versioning v2.0.0](https://semver.org/spec/v2.0.0.html).
         
        +# [1.55.0] - 2024-09-24
        +
        +## 🚀 Features
        +
        +### Support aliasing standard attributes for telemetry ([Issue #5930](https://github.com/apollographql/router/issues/5930))
        +
        +The router now supports creating aliases for standard attributes for telemetry.
        +
        +This fixes issues where standard attribute names collide with reserved attribute names. For example, the standard attribute name `entity.type` is a [reserved attribute](https://docs.newrelic.com/docs/new-relic-solutions/new-relic-one/core-concepts/what-entity-new-relic/#reserved-attributes) name for New Relic, so it won't work properly. Moreover `entity.type` is inconsistent with our other GraphQL attributes prefixed with `graphql.`
        +
        +The example configuration below renames `entity.type` to `graphql.type.name`:
        +
        +```yaml
        +telemetry:
        +  instrumentation:
        +    spans:
        +      mode: spec_compliant # Docs state this significantly improves performance: https://www.apollographql.com/docs/router/configuration/telemetry/instrumentation/spans#spec_compliant
        +    instruments:
        +      cache: # Cache instruments configuration
        +        apollo.router.operations.entity.cache: # A counter which counts the number of cache hit and miss for subgraph requests
        +          attributes:
        +            graphql.type.name: # renames entity.type
        +              alias: entity_type # ENABLED and aliased to entity_type
        +```
        +
        +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/5957
        +
        +### Enable router customizations to access demand control info ([PR #5972](https://github.com/apollographql/router/pull/5972))
        +
        +Rhai scripts and coprocessors can now access demand control information via the context. For more information on Rhai constants to access demand control info, see [available Rhai API constants](https://apollographql.com/docs/router/customizations/rhai-api#available-constants).
        +
        +By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/5972
        +
        +### Support Redis connection pooling ([PR #5942](https://github.com/apollographql/router/pull/5942))
        +
        +The router now supports Redis connection pooling for APQs, query planners and entity caches. This can improve performance when there is contention on Redis connections or latency in Redis calls.
        +
        +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/5942
        +
        +
        +## 🐛 Fixes
        +
        +### Remove unused fragments and input arguments when filtering operations ([PR #5952](https://github.com/apollographql/router/pull/5952))
        +
        +This release fixes the authorization plugin's query filtering to remove unused fragments and input arguments if the related parts of the query are removed. Previously the plugin's query filtering generated validation errors when planning certain queries.
        +
        +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/5952
        +
        +### Hot-reloads will no longer interrupt certain gauges ([PR #5996](https://github.com/apollographql/router/pull/5996), [PR #5999](https://github.com/apollographql/router/pull/5999), [PR #5999](https://github.com/apollographql/router/pull/6012))
        +
        +Previously when the router hot-reloaded a schema or a configuration file, the following gauges stopped working:
        +
        +* `apollo.router.cache.storage.estimated_size`
        +* `apollo_router_cache_size`
        +* `apollo.router.v8.heap.used`
        +* `apollo.router.v8.heap.total`
        +* `apollo.router.query_planning.queued`
        +
        +This issue has been fixed in this release, and the gauges now continue to function after a router hot-reloads.
        +
        +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5996 and https://github.com/apollographql/router/pull/5999 and https://github.com/apollographql/router/pull/6012
        +
        +### Datadog sample propagation will respect previous sampling decisions ([PR #6005](https://github.com/apollographql/router/pull/6005))
        +
        +[PR #5788](https://github.com/apollographql/router/pull/5788) introduced a regression where sampling was set on propagated headers regardless of the sampling decision in the router or upstream.
        +
        +This PR reverts the code in question and adds a test to check that a non-sampled request doesn't result in sampling in the downstream subgraph service.
        +
        +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/6005
        +
        +### Include request variables when scoring for demand control ([PR #5995](https://github.com/apollographql/router/pull/5995))
        +
        +Demand control scoring in the router now accounts for variables in queries.
        +
        +By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/5995
        +
        +## 📃 Configuration
        +
        +### Enable new and old schema introspection implementations by default ([PR #6014](https://github.com/apollographql/router/pull/6014))
        +
        +Starting with this release, if schema introspection is enabled, the router runs both the old Javascript implementation and a new Rust implementation of its introspection logic by default.
        +
        +The more performant Rust implementation will eventually replace the Javascript implementation. For now, both implementations are run by default so we can definitively assess the reliability and stability of the Rust implementation before removing the Javascript one.
        +
        +You can still toggle between implementations using the `experimental_introspection_mode` configuration key. Its valid values:
        +
        +- `new` runs only Rust-based validation
        +- `legacy` runs only Javascript-based validation
        +- `both` (default) runs both in comparison and logs errors if differences arise
        +
        +Having `both` as the default causes no client-facing impact. It will record and output the metrics of its comparison as a `apollo.router.operations.introspection.both` counter. (Note: if this counter in your metrics has `rust_error = true` or `is_matched = false`, please open an issue with Apollo.)
        +
        +Note: schema introspection itself is disabled by default, so its implementation(s) are run only if it's enabled in your configuration:
        +
        +```yaml
        +supergraph:
        +  introspection: true
        +```
        +
        +By [@SimonSapin](https://github.com/SimonSapin) in https://github.com/apollographql/router/pull/6014
        +
        +## 🧪 Experimental
        +
        +### Allow disabling persisted-queries-based query plan cache prewarm on schema reload
        +
        +The router supports the new `persisted_queries.experimental_prewarm_query_plan_cache.on_reload` configuration option. It toggles whether a query plan cache that's prewarmed upon loading a new schema includes operations from persisted query lists. Its default is `true`. Setting it `false` precludes operations from persisted query lists from being added to the prewarmed query plan cache.
        +
        +Some background about the development of this option:
        +
        +- In router v1.31.0, we started including operations from persisted query lists when the router prewarms the query plan cache when loading a new schema.
        +
        +- Then in router v1.49.0, we let you also prewarm the query plan cache from the persisted query list during router startup by setting `persisted_queries.experimental_prewarm_query_plan_cache` to true.
        +
        +- In this release, we now allow you to disable the original feature so that the router can prewarm only recent operations from the query planning cache (and not operations from persisted query lists) when loading a new schema.
        +
        +Note: the option added in v1.49.0 has been renamed from `persisted_queries.experimental_prewarm_query_plan_cache` to `persisted_queries.experimental_prewarm_query_plan_cache.on_startup`. Existing configuration files will keep working as before, but with a warning that can be resolved by updating your config file:
        +
        +```diff
        + persisted_queries:
        +   enabled: true
        +-  experimental_prewarm_query_plan_cache: true
        ++  experimental_prewarm_query_plan_cache:
        ++    on_startup: true
        +```
        +
        +By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/5990
        +
         # [1.54.0] - 2024-09-10
         
         ## 🚀 Features
        @@ -23,7 +150,7 @@ telemetry:
              logging:
                stdout:
                  enabled: true
        -         format: 
        +         format:
                    json:
                      display_span_list: false
                      span_attributes:
        @@ -50,7 +177,7 @@ By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router
         
         ### Add a histogram metric tracking evaluated query plans ([PR #5875](https://github.com/apollographql/router/pull/5875))
         
        -The router supports the new `apollo.router.query_planning.plan.evaluated_plans` histogram metric to track the number of evaluated query plans. 
        +The router supports the new `apollo.router.query_planning.plan.evaluated_plans` histogram metric to track the number of evaluated query plans.
         
         You can use it to help set an optimal `supergraph.query_planning.experimental_plans_limit` option that limits the number of query plans evaluated for a query and reduces the time spent planning.
         
        @@ -554,7 +681,7 @@ This weakness impacts all versions of Router prior to this release.  See the ass
         
         ### Provide helm support for when router's health_check's default path is not being used([Issue #5652](https://github.com/apollographql/router/issues/5652))
         
        -When helm chart is defining the liveness and readiness check probes, if the router has been configured to use a non-default health_check path, use that rather than the default ( /health ) 
        +When helm chart is defining the liveness and readiness check probes, if the router has been configured to use a non-default health_check path, use that rather than the default ( /health )
         
         By [Jon Christiansen](https://github.com/theJC) in https://github.com/apollographql/router/pull/5653
         
        @@ -562,7 +689,7 @@ By [Jon Christiansen](https://github.com/theJC) in https://github.com/apollograp
         
         Metrics of the router's entity cache have been converted to the latest format with support for custom telemetry.
         
        -The following example configuration shows the the `cache` instrument, the `cache` selector in the subgraph service, and the `cache` attribute of a subgraph span: 
        +The following example configuration shows the the `cache` instrument, the `cache` selector in the subgraph service, and the `cache` attribute of a subgraph span:
         
         ```yaml
         telemetry:
        @@ -623,7 +750,7 @@ Previously, the connection pools used by the Datadog exporter frequently timed o
         2024-07-19T15:28:22.970360Z ERROR  OpenTelemetry trace error occurred: error sending request for url (http://127.0.0.1:8126/v0.5/traces): connection error: Connection reset by peer (os error 54)
         ```
         
        -Now, the pool timeout for the Datadog exporter has been changed so that timeout errors happen much less frequently.  
        +Now, the pool timeout for the Datadog exporter has been changed so that timeout errors happen much less frequently.
         
         By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5692
         
        @@ -742,7 +869,7 @@ By [@garypen](https://github.com/garypen) in https://github.com/apollographql/ro
         ### Update router naming conventions ([PR #5400](https://github.com/apollographql/router/pull/5400))
         
         Renames our router product to distinguish between our non-commercial and commercial offerings. Instead of referring to the **Apollo Router**, we now refer to the following:
        -- **Apollo Router Core** is Apollo’s free-and-open (ELv2 licensed) implementation of a routing runtime for supergraphs. 
        +- **Apollo Router Core** is Apollo’s free-and-open (ELv2 licensed) implementation of a routing runtime for supergraphs.
         - **GraphOS Router** is based on the Apollo Router Core and fully integrated with GraphOS. GraphOS Routers provide access to GraphOS’s commercial runtime features.
         
         
        @@ -752,7 +879,7 @@ By [@shorgi](https://github.com/shorgi) in https://github.com/apollographql/rout
         
         ### Enable Rust-based API schema implementation ([PR #5623](https://github.com/apollographql/router/pull/5623))
         
        -The router has transitioned to solely using a Rust-based API schema generation implementation. 
        +The router has transitioned to solely using a Rust-based API schema generation implementation.
         
         Previously, the router used a Javascript-based implementation. After testing for a few months, we've validated the improved performance and robustness of the new Rust-based implementation, so the router now only uses it.
         
        @@ -769,7 +896,7 @@ By [@goto-bus-stop](https://github.com/goto-bus-stop) in https://github.com/apol
         The router now supports conditional execution of the coprocessor for each stage of the request lifecycle (except for the `Execution` stage).
         
         To configure, define conditions for a specific stage by using selectors based on headers or context entries. For example, based on a supergraph response you can configure the coprocessor not to execute for any subscription:
        -  
        +
         
         
         ```yaml title=router.yaml
        @@ -777,7 +904,7 @@ coprocessor:
           url: http://127.0.0.1:3000 # mandatory URL which is the address of the coprocessor
           timeout: 2s # optional timeout (2 seconds in this example). If not set, defaults to 1 second
           supergraph:
        -    response: 
        +    response:
               condition:
                 not:
                   eq:
        @@ -809,7 +936,7 @@ By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/p
         
         The router now supports the `subgraph_on_graphql_error` selector for the subgraph service, which it already supported for the router and supergraph services. Subgraph service support enables easier detection of GraphQL errors in response bodies of subgraph requests.
         
        -An example configuration with `subgraph_on_graphql_error` configured:  
        +An example configuration with `subgraph_on_graphql_error` configured:
         
         ```yaml
         telemetry:
        @@ -855,7 +982,7 @@ By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router
         
         ### Provide valid trace IDs for unsampled traces in Rhai scripts  ([PR #5606](https://github.com/apollographql/router/pull/5606))
         
        -The `traceid()` function in a Rhai script for the router now returns a valid trace ID for all traces. 
        +The `traceid()` function in a Rhai script for the router now returns a valid trace ID for all traces.
         
         Previously, `traceid()` didn't return a trace ID if the trace wasn't selected for sampling.
         
        @@ -871,7 +998,7 @@ By [@garypen](https://github.com/garypen) in https://github.com/apollographql/ro
         
         This router now gracefully handles responses that contain invalid "`-1`" positional values for error locations in queries by ignoring those invalid locations.
         
        -This change resolves the problem of GraphQL Java and GraphQL Kotlin using `{ "line": -1, "column": -1 }` values if they can't determine an error's location in a query, but the GraphQL specification [requires both `line` and `column` to be positive numbers](https://spec.graphql.org/draft/#sel-GAPHRPFCCaCGX5zM).  
        +This change resolves the problem of GraphQL Java and GraphQL Kotlin using `{ "line": -1, "column": -1 }` values if they can't determine an error's location in a query, but the GraphQL specification [requires both `line` and `column` to be positive numbers](https://spec.graphql.org/draft/#sel-GAPHRPFCCaCGX5zM).
         
         As an example, a subgraph can respond with invalid error locations:
         ```json
        @@ -905,7 +1032,7 @@ By [@IvanGoncharov](https://github.com/IvanGoncharov) in https://github.com/apol
         
         The router now returns request timeout errors (`408 Request Timeout`) and request rate limited errors (`429 Too Many Requests`) as structured GraphQL errors (for example, `{"errors": [...]}`). Previously, the router returned these as plaintext errors to clients.
         
        -Both types of errors are properly tracked in telemetry, including the `apollo_router_graphql_error_total` metric. 
        +Both types of errors are properly tracked in telemetry, including the `apollo_router_graphql_error_total` metric.
         
         By [@IvanGoncharov](https://github.com/IvanGoncharov) in https://github.com/apollographql/router/pull/5578
         
        @@ -990,7 +1117,7 @@ By [@andrewmcgivery](https://github.com/andrewmcgivery) in https://github.com/ap
         
         ### Support local persisted query manifests for use with offline licenses ([Issue #4587](https://github.com/apollographql/router/issues/4587))
         
        -Adds experimental support for passing [persisted query manifests](https://www.apollographql.com/docs/graphos/operations/persisted-queries/#31-generate-persisted-query-manifests) to use instead of the hosted Uplink version. 
        +Adds experimental support for passing [persisted query manifests](https://www.apollographql.com/docs/graphos/operations/persisted-queries/#31-generate-persisted-query-manifests) to use instead of the hosted Uplink version.
         
         For example:
         
        @@ -998,7 +1125,7 @@ For example:
         persisted_queries:
           enabled: true
           log_unknown: true
        -  experimental_local_manifests: 
        +  experimental_local_manifests:
             - ./persisted-query-manifest.json
           safelist:
             enabled: true
        @@ -1033,7 +1160,7 @@ By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router
         
         ### Make `status_code` available for `router_service` responses in Rhai scripts ([Issue #5357](https://github.com/apollographql/router/issues/5357))
         
        -Adds `response.status_code` on Rhai [`router_service`](https://www.apollographql.com/docs/router/customizations/rhai-api/#entry-point-hooks) responses. Previously, `status_code` was only available on `subgraph_service` responses. 
        +Adds `response.status_code` on Rhai [`router_service`](https://www.apollographql.com/docs/router/customizations/rhai-api/#entry-point-hooks) responses. Previously, `status_code` was only available on `subgraph_service` responses.
         
         For example:
         
        @@ -1132,11 +1259,11 @@ By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/p
         
         Improves accuracy and performance of event telemetry by:
         
        -- Displaying custom event attributes even if the trace is not sampled 
        +- Displaying custom event attributes even if the trace is not sampled
         - Preserving original attribute type instead of converting it to string
         - Ensuring `http.response.body.size` and `http.request.body.size` attributes are treated as numbers, not strings
         
        -> :warning: Exercise caution if you have monitoring enabled on your logs, as attribute types may have changed. For example, attributes like `http.response.status_code` are now numbers (`200`) instead of strings (`"200"`). 
        +> :warning: Exercise caution if you have monitoring enabled on your logs, as attribute types may have changed. For example, attributes like `http.response.status_code` are now numbers (`200`) instead of strings (`"200"`).
         
         
         By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/5464
        @@ -1150,7 +1277,7 @@ By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router
         ### Improve accuracy of `query_planning.plan.duration` ([PR #5](https://github.com/apollographql/router/pull/5530))
         Previously, the `apollo.router.query_planning.plan.duration` metric inaccurately included additional processing time beyond query planning. The additional time included pooling time, which is already accounted for in the metric. After this update, apollo.router.query_planning.plan.duration now accurately reflects only the query planning duration without additional processing time.
         
        -For example, before the change, metrics reported: 
        +For example, before the change, metrics reported:
         ```bash
         2024-06-21T13:37:27.744592Z WARN  apollo.router.query_planning.plan.duration 0.002475708
         2024-06-21T13:37:27.744651Z WARN  apollo.router.query_planning.total.duration 0.002553958
        @@ -1218,19 +1345,19 @@ telemetry:
                 # OLD definition of a custom instrument that measures the number of fields
                 my.unit.instrument:
                   value: field_unit # Changes to unit
        -        
        +
                 # NEW definition
                 my.unit.instrument:
        -          value: unit 
        +          value: unit
         
        -        # OLD  
        +        # OLD
                 my.custom.instrument:
                   value: # Changes to not require `field_custom`
                     field_custom:
                       list_length: value
                 # NEW
                 my.custom.instrument:
        -          value: 
        +          value:
                     list_length: value
         ```
         
        @@ -1359,7 +1486,7 @@ Allows HTTP/2 Cleartext (h2c) communication with coprocessors for scenarios wher
         Introduces a new `coprocessor.client` configuration. The first and currently only option is `experimental_http2`. The available option settings are the same as the as [`experimental_http2` traffic shaping settings](https://www.apollographql.com/docs/router/configuration/traffic-shaping/#http2).
         
         - `disable` - disable HTTP/2, use HTTP/1.1 only
        -- `enable` - HTTP URLs use HTTP/1.1, HTTPS URLs use TLS with either HTTP/1.1 or HTTP/2 based on the TLS handshake 
        +- `enable` - HTTP URLs use HTTP/1.1, HTTPS URLs use TLS with either HTTP/1.1 or HTTP/2 based on the TLS handshake
         - `http2only` - HTTP URLs use h2c, HTTPS URLs use TLS with HTTP/2
         - not set - defaults to `enable`
         
        diff --git a/Cargo.lock b/Cargo.lock
        index fc611a1cc9..b72062f176 100644
        --- a/Cargo.lock
        +++ b/Cargo.lock
        @@ -178,7 +178,7 @@ dependencies = [
         
         [[package]]
         name = "apollo-federation"
        -version = "1.55.0-rc.2"
        +version = "1.55.0"
         dependencies = [
          "apollo-compiler",
          "derive_more",
        @@ -229,7 +229,7 @@ dependencies = [
         
         [[package]]
         name = "apollo-router"
        -version = "1.55.0-rc.2"
        +version = "1.55.0"
         dependencies = [
          "access-json",
          "ahash",
        @@ -400,7 +400,7 @@ dependencies = [
         
         [[package]]
         name = "apollo-router-benchmarks"
        -version = "1.55.0-rc.2"
        +version = "1.55.0"
         dependencies = [
          "apollo-parser",
          "apollo-router",
        @@ -416,7 +416,7 @@ dependencies = [
         
         [[package]]
         name = "apollo-router-scaffold"
        -version = "1.55.0-rc.2"
        +version = "1.55.0"
         dependencies = [
          "anyhow",
          "cargo-scaffold",
        diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml
        index 504c350ee8..5de40a5380 100644
        --- a/apollo-federation/Cargo.toml
        +++ b/apollo-federation/Cargo.toml
        @@ -1,6 +1,6 @@
         [package]
         name = "apollo-federation"
        -version = "1.55.0-rc.2"
        +version = "1.55.0"
         authors = ["The Apollo GraphQL Contributors"]
         edition = "2021"
         description = "Apollo Federation"
        diff --git a/apollo-router-benchmarks/Cargo.toml b/apollo-router-benchmarks/Cargo.toml
        index 523790860d..38759d2feb 100644
        --- a/apollo-router-benchmarks/Cargo.toml
        +++ b/apollo-router-benchmarks/Cargo.toml
        @@ -1,6 +1,6 @@
         [package]
         name = "apollo-router-benchmarks"
        -version = "1.55.0-rc.2"
        +version = "1.55.0"
         authors = ["Apollo Graph, Inc. "]
         edition = "2021"
         license = "Elastic-2.0"
        diff --git a/apollo-router-scaffold/Cargo.toml b/apollo-router-scaffold/Cargo.toml
        index aa0eacd8b0..4801a6c616 100644
        --- a/apollo-router-scaffold/Cargo.toml
        +++ b/apollo-router-scaffold/Cargo.toml
        @@ -1,6 +1,6 @@
         [package]
         name = "apollo-router-scaffold"
        -version = "1.55.0-rc.2"
        +version = "1.55.0"
         authors = ["Apollo Graph, Inc. "]
         edition = "2021"
         license = "Elastic-2.0"
        diff --git a/apollo-router-scaffold/templates/base/Cargo.template.toml b/apollo-router-scaffold/templates/base/Cargo.template.toml
        index fe7de1521c..a1c3d2a42d 100644
        --- a/apollo-router-scaffold/templates/base/Cargo.template.toml
        +++ b/apollo-router-scaffold/templates/base/Cargo.template.toml
        @@ -22,7 +22,7 @@ apollo-router = { path ="{{integration_test}}apollo-router" }
         apollo-router = { git="https://github.com/apollographql/router.git", branch="{{branch}}" }
         {{else}}
         # Note if you update these dependencies then also update xtask/Cargo.toml
        -apollo-router = "1.55.0-rc.2"
        +apollo-router = "1.55.0"
         {{/if}}
         {{/if}}
         async-trait = "0.1.52"
        diff --git a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml
        index 23a17e421b..c566a3a0ca 100644
        --- a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml
        +++ b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml
        @@ -13,7 +13,7 @@ apollo-router-scaffold = { path ="{{integration_test}}apollo-router-scaffold" }
         {{#if branch}}
         apollo-router-scaffold = { git="https://github.com/apollographql/router.git", branch="{{branch}}" }
         {{else}}
        -apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.55.0-rc.2" }
        +apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.55.0" }
         {{/if}}
         {{/if}}
         anyhow = "1.0.58"
        diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml
        index 4f4aa76ce5..4675c9a5d7 100644
        --- a/apollo-router/Cargo.toml
        +++ b/apollo-router/Cargo.toml
        @@ -1,6 +1,6 @@
         [package]
         name = "apollo-router"
        -version = "1.55.0-rc.2"
        +version = "1.55.0"
         authors = ["Apollo Graph, Inc. "]
         repository = "https://github.com/apollographql/router/"
         documentation = "https://docs.rs/apollo-router"
        @@ -68,7 +68,7 @@ askama = "0.12.1"
         access-json = "0.1.0"
         anyhow = "1.0.86"
         apollo-compiler.workspace = true
        -apollo-federation = { path = "../apollo-federation", version = "=1.55.0-rc.2" }
        +apollo-federation = { path = "../apollo-federation", version = "=1.55.0" }
         arc-swap = "1.6.0"
         async-channel = "1.9.0"
         async-compression = { version = "0.4.6", features = [
        diff --git a/dockerfiles/tracing/docker-compose.datadog.yml b/dockerfiles/tracing/docker-compose.datadog.yml
        index e59725accb..3b58c1d702 100644
        --- a/dockerfiles/tracing/docker-compose.datadog.yml
        +++ b/dockerfiles/tracing/docker-compose.datadog.yml
        @@ -3,7 +3,7 @@ services:
         
           apollo-router:
             container_name: apollo-router
        -    image: ghcr.io/apollographql/router:v1.55.0-rc.2
        +    image: ghcr.io/apollographql/router:v1.55.0
             volumes:
               - ./supergraph.graphql:/etc/config/supergraph.graphql
               - ./router/datadog.router.yaml:/etc/config/configuration.yaml
        diff --git a/dockerfiles/tracing/docker-compose.jaeger.yml b/dockerfiles/tracing/docker-compose.jaeger.yml
        index ac96c102fe..fcb75930d2 100644
        --- a/dockerfiles/tracing/docker-compose.jaeger.yml
        +++ b/dockerfiles/tracing/docker-compose.jaeger.yml
        @@ -4,7 +4,7 @@ services:
           apollo-router:
             container_name: apollo-router
             #build: ./router
        -    image: ghcr.io/apollographql/router:v1.55.0-rc.2
        +    image: ghcr.io/apollographql/router:v1.55.0
             volumes:
               - ./supergraph.graphql:/etc/config/supergraph.graphql
               - ./router/jaeger.router.yaml:/etc/config/configuration.yaml
        diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml
        index a3f2844a49..0cb933a7ac 100644
        --- a/dockerfiles/tracing/docker-compose.zipkin.yml
        +++ b/dockerfiles/tracing/docker-compose.zipkin.yml
        @@ -4,7 +4,7 @@ services:
           apollo-router:
             container_name: apollo-router
             build: ./router
        -    image: ghcr.io/apollographql/router:v1.55.0-rc.2
        +    image: ghcr.io/apollographql/router:v1.55.0
             volumes:
               - ./supergraph.graphql:/etc/config/supergraph.graphql
               - ./router/zipkin.router.yaml:/etc/config/configuration.yaml
        diff --git a/helm/chart/router/Chart.yaml b/helm/chart/router/Chart.yaml
        index 4e1453f903..7a1c6d615a 100644
        --- a/helm/chart/router/Chart.yaml
        +++ b/helm/chart/router/Chart.yaml
        @@ -20,10 +20,10 @@ type: application
         # so it matches the shape of our release process and release automation.
         # By proxy of that decision, this version uses SemVer 2.0.0, though the prefix
         # of "v" is not included.
        -version: 1.55.0-rc.2
        +version: 1.55.0
         
         # This is the version number of the application being deployed. This version number should be
         # incremented each time you make changes to the application. Versions are not expected to
         # follow Semantic Versioning. They should reflect the version the application is using.
         # It is recommended to use it with quotes.
        -appVersion: "v1.55.0-rc.2"
        +appVersion: "v1.55.0"
        diff --git a/helm/chart/router/README.md b/helm/chart/router/README.md
        index 5b3abde642..fb09765b10 100644
        --- a/helm/chart/router/README.md
        +++ b/helm/chart/router/README.md
        @@ -2,7 +2,7 @@
         
         [router](https://github.com/apollographql/router) Rust Graph Routing runtime for Apollo Federation
         
        -![Version: 1.55.0-rc.2](https://img.shields.io/badge/Version-1.55.0--rc.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.55.0-rc.2](https://img.shields.io/badge/AppVersion-v1.55.0--rc.2-informational?style=flat-square)
        +![Version: 1.55.0](https://img.shields.io/badge/Version-1.55.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.55.0](https://img.shields.io/badge/AppVersion-v1.55.0-informational?style=flat-square)
         
         ## Prerequisites
         
        @@ -11,7 +11,7 @@
         ## Get Repo Info
         
         ```console
        -helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0-rc.2
        +helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0
         ```
         
         ## Install Chart
        @@ -19,7 +19,7 @@ helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0-rc.2
         **Important:** only helm3 is supported
         
         ```console
        -helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0-rc.2 --values my-values.yaml
        +helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0 --values my-values.yaml
         ```
         
         _See [configuration](#configuration) below._
        diff --git a/licenses.html b/licenses.html
        index 30647c049e..e415d536c4 100644
        --- a/licenses.html
        +++ b/licenses.html
        @@ -44,8 +44,8 @@ 

        Third Party Licenses

        Overview of licenses:

        +
      • +

        Apache License 2.0

        +

        Used by:

        + +
        ../../LICENSE-APACHE
        +
      • Apache License 2.0

        Used by:

        @@ -11507,13 +11518,11 @@

        Apache License 2.0

        Used by:

      • + +
      • +

        Apache License 2.0

        +

        Used by:

        + +
        Copyright [2022] [Bryn Cooke]
        +
        +Licensed under the Apache License, Version 2.0 (the "License");
        +you may not use this file except in compliance with the License.
        +You may obtain a copy of the License at
        +
        +    http://www.apache.org/licenses/LICENSE-2.0
        +
         Unless required by applicable law or agreed to in writing, software
         distributed under the License is distributed on an "AS IS" BASIS,
         WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        @@ -13299,6 +13329,66 @@ 

        Used by:

        shall be included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +
        +
      • +
      • +

        MIT License

        +

        Used by:

        + +
        Copyright (c) 2019 Carl Lerche
        +
        +Permission is hereby granted, free of charge, to any
        +person obtaining a copy of this software and associated
        +documentation files (the "Software"), to deal in the
        +Software without restriction, including without
        +limitation the rights to use, copy, modify, merge,
        +publish, distribute, sublicense, and/or sell copies of
        +the Software, and to permit persons to whom the Software
        +is furnished to do so, subject to the following
        +conditions:
        +
        +The above copyright notice and this permission notice
        +shall be included in all copies or substantial portions
        +of the Software.
        +
        +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
        +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
        +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
        +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
        +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
        +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
        +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
        +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
        +DEALINGS IN THE SOFTWARE.
        +
        +Copyright (c) 2018 David Tolnay
        +
        +Permission is hereby granted, free of charge, to any
        +person obtaining a copy of this software and associated
        +documentation files (the "Software"), to deal in the
        +Software without restriction, including without
        +limitation the rights to use, copy, modify, merge,
        +publish, distribute, sublicense, and/or sell copies of
        +the Software, and to permit persons to whom the Software
        +is furnished to do so, subject to the following
        +conditions:
        +
        +The above copyright notice and this permission notice
        +shall be included in all copies or substantial portions
        +of the Software.
        +
         THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
         ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
         TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
        @@ -14305,8 +14395,6 @@ 

        Used by:

        MIT License

        Used by:

          -
        • async-stream
        • -
        • async-stream-impl
        • base64-simd
        • convert_case
        • cookie-factory
        • @@ -14649,6 +14737,8 @@

          Used by:

          The MIT License (MIT)
           
          @@ -15177,6 +15267,8 @@ 

          Used by:

          MIT License

          Used by:

            +
          • aho-corasick
          • +
          • byteorder
          • globset
          • memchr
          • regex-automata
          • @@ -15584,7 +15676,6 @@

            Used by:

            Mozilla Public License 2.0

            Used by:

            Mozilla Public License Version 2.0
            @@ -15967,6 +16058,7 @@ 

            Used by:

            Mozilla Public License 2.0

            Used by:

            Mozilla Public License Version 2.0
            diff --git a/scripts/install.sh b/scripts/install.sh
            index 1dca7b24db..f03b383d8d 100755
            --- a/scripts/install.sh
            +++ b/scripts/install.sh
            @@ -11,7 +11,7 @@ BINARY_DOWNLOAD_PREFIX="https://github.com/apollographql/router/releases/downloa
             
             # Router version defined in apollo-router's Cargo.toml
             # Note: Change this line manually during the release steps.
            -PACKAGE_VERSION="v1.55.0-rc.2"
            +PACKAGE_VERSION="v1.55.0"
             
             download_binary() {
                 downloader --check
            
            From 04dc53980d9f1a1f1564536ab46c641a8fa6aa57 Mon Sep 17 00:00:00 2001
            From: Jesse Rosenberger 
            Date: Tue, 24 Sep 2024 18:11:27 +0300
            Subject: [PATCH 52/56] Bump CircleCI cache key
            
            ---
             .circleci/config.yml | 1 +
             1 file changed, 1 insertion(+)
            
            diff --git a/.circleci/config.yml b/.circleci/config.yml
            index 6c4daf0a3d..15fe0c803c 100644
            --- a/.circleci/config.yml
            +++ b/.circleci/config.yml
            @@ -1,5 +1,6 @@
             version: 2.1
             
            +
             # These "CircleCI Orbs" are reusable bits of configuration that can be shared
             # across projects.  See https://circleci.com/orbs/ for more information.
             orbs:
            
            From 05dfe42cb8eaea3c045df701a923dc948ad17e27 Mon Sep 17 00:00:00 2001
            From: Jesse Rosenberger 
            Date: Tue, 24 Sep 2024 18:14:12 +0300
            Subject: [PATCH 53/56] Update CHANGELOG.md to include the warning for cache
             key bumps.
            
            ---
             CHANGELOG.md | 3 +++
             1 file changed, 3 insertions(+)
            
            diff --git a/CHANGELOG.md b/CHANGELOG.md
            index 73dc9ffd54..419cee0eb9 100644
            --- a/CHANGELOG.md
            +++ b/CHANGELOG.md
            @@ -6,6 +6,9 @@ This project adheres to [Semantic Versioning v2.0.0](https://semver.org/spec/v2.
             
             # [1.55.0] - 2024-09-24
             
            +> [!IMPORTANT]
            +> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), this release changes the hashing algorithm used for the cache keys.  On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new hashing algorithm comes into service.
            +
             ## 🚀 Features
             
             ### Support aliasing standard attributes for telemetry ([Issue #5930](https://github.com/apollographql/router/issues/5930))
            
            From d5c1cfa6d09db706cef5556392ce33f37a5fd5c3 Mon Sep 17 00:00:00 2001
            From: Jesse Rosenberger 
            Date: Tue, 24 Sep 2024 19:02:26 +0300
            Subject: [PATCH 54/56] Add another line to the CircleCI cache to hopefully
             bump.
            
            ---
             .circleci/config.yml | 1 +
             1 file changed, 1 insertion(+)
            
            diff --git a/.circleci/config.yml b/.circleci/config.yml
            index 15fe0c803c..19da85a3c6 100644
            --- a/.circleci/config.yml
            +++ b/.circleci/config.yml
            @@ -1,5 +1,6 @@
             version: 2.1
             
            +# TODO Remove this line.
             
             # These "CircleCI Orbs" are reusable bits of configuration that can be shared
             # across projects.  See https://circleci.com/orbs/ for more information.
            
            From e6a2f2b38acc79c2eadc81e7691a4657adcc4a71 Mon Sep 17 00:00:00 2001
            From: Jesse Rosenberger 
            Date: Tue, 24 Sep 2024 19:07:06 +0300
            Subject: [PATCH 55/56] Raise macos resource_class for test and build
            
            ---
             .circleci/config.yml | 4 ++--
             1 file changed, 2 insertions(+), 2 deletions(-)
            
            diff --git a/.circleci/config.yml b/.circleci/config.yml
            index 19da85a3c6..ac9ea0f671 100644
            --- a/.circleci/config.yml
            +++ b/.circleci/config.yml
            @@ -49,7 +49,7 @@ executors:
                   # at https://circleci.com/docs/using-macos#supported-xcode-versions.
                   # We use the major.minor notation to bring in compatible patches.
                   xcode: "14.2.0"
            -    resource_class: macos.m1.medium.gen1
            +    resource_class: macos.m1.large.gen1
               macos_test: &macos_test_executor
                 macos:
                   # See https://circleci.com/docs/xcode-policy along with the support matrix
            @@ -60,7 +60,7 @@ executors:
                   # once we update to Xcode >= 15.1.0 
                   # See: https://github.com/apollographql/router/pull/5462
                   xcode: "14.2.0"
            -    resource_class: macos.m1.medium.gen1
            +    resource_class: macos.m1.large.gen1
               windows_build: &windows_build_executor
                 machine:
                   image: "windows-server-2019-vs2019:2024.02.21"
            
            From 2ef53bdd6e489131e9a80a13ac71fc443c0c7f9d Mon Sep 17 00:00:00 2001
            From: Geoffroy Couprie 
            Date: Tue, 24 Sep 2024 19:40:06 +0200
            Subject: [PATCH 56/56] deactivate test_subgraph__service_panic on OSX (#6049)
            
            ---
             apollo-router/src/services/subgraph_service.rs | 2 ++
             1 file changed, 2 insertions(+)
            
            diff --git a/apollo-router/src/services/subgraph_service.rs b/apollo-router/src/services/subgraph_service.rs
            index 5d8fae1ede..2b99f3bd24 100644
            --- a/apollo-router/src/services/subgraph_service.rs
            +++ b/apollo-router/src/services/subgraph_service.rs
            @@ -1744,6 +1744,7 @@ mod tests {
                 }
             
                 // starts a local server emulating a subgraph returning connection closed
            +    #[cfg(not(target_os = "macos"))]
                 async fn emulate_subgraph_panic(listener: TcpListener) {
                     async fn handle(_request: http::Request) -> Result, Infallible> {
                         panic!("test")
            @@ -2471,6 +2472,7 @@ mod tests {
                 }
             
                 #[tokio::test(flavor = "multi_thread")]
            +    #[cfg(not(target_os = "macos"))]
                 async fn test_subgraph_service_panic() {
                     let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
                     let socket_addr = listener.local_addr().unwrap();