diff --git a/.changesets/chore_update_router_bridge.md b/.changesets/chore_update_router_bridge.md deleted file mode 100644 index bc23827874..0000000000 --- a/.changesets/chore_update_router_bridge.md +++ /dev/null @@ -1,8 +0,0 @@ -> [!IMPORTANT] -> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), this release changes the hashing algorithm used for the cache keys. On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new hashing algorithm comes into service. - -### Update to Federation v2.9.1 ([PR #6029](https://github.com/apollographql/router/pull/6029)) - -This release updates to Federation v2.9.1, which fixes edge cases in subgraph extraction logic when using spec renaming or spec URLs (e.g., `specs.apollo.dev`) that could impact the planner's ability to plan a query. - -By [@lrlna](https://github.com/lrlna) in https://github.com/apollographql/router/pull/6027 diff --git a/.circleci/config.yml b/.circleci/config.yml index ac9ea0f671..d2be4bcd00 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,5 @@ 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. orbs: @@ -358,9 +356,9 @@ commands: equal: [ *arm_linux_test_executor, << parameters.platform >> ] steps: - run: - name: Install nightly Rust to build the fuzzers + name: Install nightly-2024-09-22 Rust to build the fuzzers command: | - rustup install nightly + rustup install nightly-2024-09-22 install_extra_tools: steps: @@ -526,7 +524,7 @@ commands: path: ./target/nextest/ci/junit.xml fuzz_build: steps: - - run: cargo +nightly fuzz build + - run: cargo +nightly-2024-09-22 fuzz build jobs: lint: diff --git a/CHANGELOG.md b/CHANGELOG.md index 419cee0eb9..24f87558fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,85 @@ All notable changes to Router will be documented in this file. This project adheres to [Semantic Versioning v2.0.0](https://semver.org/spec/v2.0.0.html). +# [1.56.0] - 2024-10-01 + +> [!IMPORTANT] +> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), this release changes the hashing algorithm used for the cache keys. On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new hashing algorithm comes into service. + +## 🚀 Features + +## Native query planner is now in public preview + +The native query planner is now in public preview. You can configure the `experimental_query_planner_mode` option in the router configuration YAML to change the mode of the native query planner. The following modes are available: + +- `new`: Enable _only_ the new Rust-native query planner in the hot-path of query execution. +- `legacy`: Enable _only_ the legacy JavaScript query planner in the hot-path of query execution. +- `both_best_effort`: Enables _both_ the new and legacy query planners. They are configured in a comparison-based mode of operation with the legacy planner in the hot-path and the and the new planner in the cold-path. Comparisons are made between the two plans on a sampled basis and metrics are available to analyze the differences in aggregate. + +### Support loading Apollo key from file ([PR #5917](https://github.com/apollographql/router/pull/5917)) + +You can now specific the location to a file containing the Apollo key that's used by Apollo Uplink and usage reporting. The router now supports both the `--apollo-key-path` CLI argument and the `APOLLO_KEY_PATH` environment variable for passing the file containing your Apollo key. + +Previously, the router supported only the `APOLLO_KEY` environment variable to provide the key. The new CLI argument and environment variable help users who prefer not to pass sensitive keys through environment variables. + +Note: This feature is unavailable for Windows. + +By [@lleadbet](https://github.com/lleadbet) in https://github.com/apollographql/router/pull/5917 + + +## 🐛 Fixes + +### Prevent sending internal `apollo_private.*` attributes to Jaeger collector ([PR #6033](https://github.com/apollographql/router/pull/6033)) + +When using the router's Jaeger collector to send traces, you will no longer receive span attributes with the `apollo_private.` prefix. Those attributes were incorrectly sent, as that prefix is reserved for internal attributes. + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6033 + +### Fix displaying custom event attributes on subscription events ([PR #6033](https://github.com/apollographql/router/pull/6033)) + +The router now properly displays custom event attributes that are set with selectors at the supergraph level. + +An example configuration: + +```yaml title=router.yaml +telemetry: + instrumentation: + events: + supergraph: + supergraph.event: + message: supergraph event + on: event_response # on every supergraph event (like subscription event for example) + level: info + attributes: + test: + static: foo + response.data: + response_data: $ # Display all the response data payload + response.errors: + response_errors: $ # Display all the response errors payload +``` + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6033 + +### Update to Federation v2.9.2 ([PR #6069](https://github.com/apollographql/router/pull/6069)) + +This release updates to Federation v2.9.2, with a small fix to the internal `__typename` optimization and a fix to prevent argument name collisions in the `@context`/`@fromContext` directives. + +By [@dariuszkuc](https://github.com/dariuszkuc) in https://github.com/apollographql/router/pull/6069 + +## 📃 Configuration + +### Add metrics for Rust vs. Deno configuration values ([PR #6056](https://github.com/apollographql/router/pull/6056)) + +To help track the migration from JavaScript (Deno) to native Rust implementations, the router now reports the values of the following configuration options to Apollo: + +- `apollo.router.config.experimental_query_planner_mode` +- `apollo.router.config.experimental_introspection_mode` + +By [@goto-bus-stop](https://github.com/goto-bus-stop) in https://github.com/apollographql/router/pull/6056 + + + # [1.55.0] - 2024-09-24 > [!IMPORTANT] diff --git a/Cargo.lock b/Cargo.lock index b72062f176..8c5d20f688 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,9 +159,9 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "apollo-compiler" -version = "1.0.0-beta.23" +version = "1.0.0-beta.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875f39060728ac3e775fc3fe5421225d6df92c4d5155a9524cdb198f05006d36" +checksum = "71153ad85c85f7aa63f0e0a5868912c220bb48e4c764556f5841d37fc17b0103" dependencies = [ "ahash", "apollo-parser", @@ -178,7 +178,7 @@ dependencies = [ [[package]] name = "apollo-federation" -version = "1.55.0" +version = "1.56.0" dependencies = [ "apollo-compiler", "derive_more", @@ -229,7 +229,7 @@ dependencies = [ [[package]] name = "apollo-router" -version = "1.55.0" +version = "1.56.0" dependencies = [ "access-json", "ahash", @@ -268,7 +268,6 @@ dependencies = [ "derive_more", "dhat", "diff", - "directories", "displaydoc", "ecdsa", "flate2", @@ -400,7 +399,7 @@ dependencies = [ [[package]] name = "apollo-router-benchmarks" -version = "1.55.0" +version = "1.56.0" dependencies = [ "apollo-parser", "apollo-router", @@ -416,7 +415,7 @@ dependencies = [ [[package]] name = "apollo-router-scaffold" -version = "1.55.0" +version = "1.56.0" dependencies = [ "anyhow", "cargo-scaffold", @@ -448,9 +447,9 @@ dependencies = [ [[package]] name = "apollo-smith" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40cff1a5989a471714cfdf53f24d0948b7f77631ab3dbd25b2f6eacbf58e5261" +checksum = "d89479524886fdbe62b124d3825879778680e0147304d1a6d32164418f8089a2" dependencies = [ "apollo-compiler", "apollo-parser", @@ -2303,15 +2302,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "directories" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" -dependencies = [ - "dirs-sys", -] - [[package]] name = "dirs" version = "5.0.1" @@ -5609,9 +5599,9 @@ dependencies = [ [[package]] name = "router-bridge" -version = "0.6.2+v2.9.1" +version = "0.6.3+v2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82c217157e756750386a5371da31590c89c315d953ae6d299d73949138e332f" +checksum = "2f183e217b4010e7d37d581b7919ca5e0136a46b6d6b1ff297c52e702bce1089" dependencies = [ "anyhow", "async-channel 1.9.0", diff --git a/Cargo.toml b/Cargo.toml index 0352205ebb..e48b91c854 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,9 +49,9 @@ debug = 1 # Dependencies used in more than one place are specified here in order to keep versions in sync: # https://doc.rust-lang.org/cargo/reference/workspaces.html#the-dependencies-table [workspace.dependencies] -apollo-compiler = "=1.0.0-beta.23" +apollo-compiler = "=1.0.0-beta.24" apollo-parser = "0.8.0" -apollo-smith = "0.13.0" +apollo-smith = "0.14.0" async-trait = "0.1.77" hex = { version = "0.4.3", features = ["serde"] } http = "0.2.11" diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml index 5de40a5380..448a7398cd 100644 --- a/apollo-federation/Cargo.toml +++ b/apollo-federation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-federation" -version = "1.55.0" +version = "1.56.0" authors = ["The Apollo GraphQL Contributors"] edition = "2021" description = "Apollo Federation" diff --git a/apollo-federation/src/compat.rs b/apollo-federation/src/compat.rs index 701337714c..d4faa04880 100644 --- a/apollo-federation/src/compat.rs +++ b/apollo-federation/src/compat.rs @@ -24,11 +24,13 @@ use apollo_compiler::Schema; fn is_semantic_directive_application(directive: &Directive) -> bool { match directive.name.as_str() { "specifiedBy" => true, - // For @deprecated, explicitly writing `reason: null` disables the directive, - // as `null` overrides the default string value. + // graphql-js’ intropection returns `isDeprecated: false` for `@deprecated(reason: null)`, + // which is arguably a bug. Do the same here for now. + // TODO: remove this and allow `isDeprecated: true`, `deprecatedReason: null` + // after we fully move to Rust introspection? "deprecated" if directive - .argument_by_name("reason") + .specified_argument_by_name("reason") .is_some_and(|value| value.is_null()) => { false @@ -42,7 +44,7 @@ fn is_semantic_directive_application(directive: &Directive) -> bool { fn standardize_deprecated(directive: &mut Directive) { if directive.name == "deprecated" && directive - .argument_by_name("reason") + .specified_argument_by_name("reason") .and_then(|value| value.as_str()) .is_some_and(|reason| reason == "No longer supported") { diff --git a/apollo-federation/src/link/argument.rs b/apollo-federation/src/link/argument.rs index 662cd0a08d..12702dadb2 100644 --- a/apollo-federation/src/link/argument.rs +++ b/apollo-federation/src/link/argument.rs @@ -13,7 +13,7 @@ pub(crate) fn directive_optional_enum_argument( application: &Node, name: &Name, ) -> Result, FederationError> { - match application.argument_by_name(name) { + match application.specified_argument_by_name(name) { Some(value) => match value.deref() { Value::Enum(name) => Ok(Some(name.clone())), Value::Null => Ok(None), @@ -48,7 +48,7 @@ pub(crate) fn directive_optional_string_argument<'doc>( application: &'doc Node, name: &Name, ) -> Result, FederationError> { - match application.argument_by_name(name) { + match application.specified_argument_by_name(name) { Some(value) => match value.deref() { Value::String(name) => Ok(Some(name)), Value::Null => Ok(None), @@ -83,7 +83,7 @@ pub(crate) fn directive_optional_boolean_argument( application: &Node, name: &Name, ) -> Result, FederationError> { - match application.argument_by_name(name) { + match application.specified_argument_by_name(name) { Some(value) => match value.deref() { Value::Boolean(value) => Ok(Some(*value)), Value::Null => Ok(None), @@ -119,7 +119,7 @@ pub(crate) fn directive_optional_variable_boolean_argument( application: &Node, name: &Name, ) -> Result, FederationError> { - match application.argument_by_name(name) { + match application.specified_argument_by_name(name) { Some(value) => match value.deref() { Value::Variable(name) => Ok(Some(BooleanOrVariable::Variable(name.clone()))), Value::Boolean(value) => Ok(Some(BooleanOrVariable::Boolean(*value))), diff --git a/apollo-federation/src/link/database.rs b/apollo-federation/src/link/database.rs index 94ea7ba0ff..ced0dc7b07 100644 --- a/apollo-federation/src/link/database.rs +++ b/apollo-federation/src/link/database.rs @@ -33,10 +33,10 @@ pub fn links_metadata(schema: &Schema) -> Result, LinkErro return Err(LinkError::BootstrapError(format!( "the @link specification itself (\"{}\") is applied multiple times", extraneous_directive - .argument_by_name("url") + .specified_argument_by_name("url") // XXX(@goto-bus-stop): @core compatibility is primarily to support old tests in other projects, // and should be removed when those are updated. - .or(extraneous_directive.argument_by_name("feature")) + .or(extraneous_directive.specified_argument_by_name("feature")) .and_then(|value| value.as_str().map(Cow::Borrowed)) .unwrap_or_else(|| Cow::Owned(Identity::link_identity().to_string())) ))); @@ -184,13 +184,13 @@ fn is_bootstrap_directive(schema: &Schema, directive: &Directive) -> bool { }; if is_link_directive_definition(definition) { if let Some(url) = directive - .argument_by_name("url") + .specified_argument_by_name("url") .and_then(|value| value.as_str()) { let url = url.parse::(); let default_link_name = DEFAULT_LINK_NAME; let expected_name = directive - .argument_by_name("as") + .specified_argument_by_name("as") .and_then(|value| value.as_str()) .unwrap_or(default_link_name.as_str()); return url.map_or(false, |url| { @@ -201,12 +201,12 @@ fn is_bootstrap_directive(schema: &Schema, directive: &Directive) -> bool { // XXX(@goto-bus-stop): @core compatibility is primarily to support old tests--should be // removed when those are updated. if let Some(url) = directive - .argument_by_name("feature") + .specified_argument_by_name("feature") .and_then(|value| value.as_str()) { let url = url.parse::(); let expected_name = directive - .argument_by_name("as") + .specified_argument_by_name("as") .and_then(|value| value.as_str()) .unwrap_or("core"); return url.map_or(false, |url| { diff --git a/apollo-federation/src/link/mod.rs b/apollo-federation/src/link/mod.rs index 96473e59db..76a59da2ea 100644 --- a/apollo-federation/src/link/mod.rs +++ b/apollo-federation/src/link/mod.rs @@ -275,9 +275,9 @@ impl Link { } pub fn from_directive_application(directive: &Node) -> Result { - let (url, is_link) = if let Some(value) = directive.argument_by_name("url") { + let (url, is_link) = if let Some(value) = directive.specified_argument_by_name("url") { (value, true) - } else if let Some(value) = directive.argument_by_name("feature") { + } else if let Some(value) = directive.specified_argument_by_name("feature") { // XXX(@goto-bus-stop): @core compatibility is primarily to support old tests--should be // removed when those are updated. (value, false) @@ -303,11 +303,11 @@ impl Link { })?; let spec_alias = directive - .argument_by_name("as") + .specified_argument_by_name("as") .and_then(|arg| arg.as_str()) .map(Name::new) .transpose()?; - let purpose = if let Some(value) = directive.argument_by_name("for") { + let purpose = if let Some(value) = directive.specified_argument_by_name("for") { Some(Purpose::from_value(value)?) } else { None @@ -315,7 +315,7 @@ impl Link { let imports = if is_link { directive - .argument_by_name("import") + .specified_argument_by_name("import") .and_then(|arg| arg.as_list()) .unwrap_or(&[]) .iter() diff --git a/apollo-federation/src/operation/merging.rs b/apollo-federation/src/operation/merging.rs index c938f2e564..4c2b31cbd3 100644 --- a/apollo-federation/src/operation/merging.rs +++ b/apollo-federation/src/operation/merging.rs @@ -1,13 +1,14 @@ //! Provides methods for recursively merging selections and selection sets. use std::sync::Arc; -use selection_map::SelectionMap; +use apollo_compiler::collections::IndexMap; use super::selection_map; use super::FieldSelection; use super::FieldSelectionValue; use super::FragmentSpreadSelection; use super::FragmentSpreadSelectionValue; +use super::HasSelectionKey as _; use super::InlineFragmentSelection; use super::InlineFragmentSelectionValue; use super::NamedFragments; @@ -15,8 +16,6 @@ use super::Selection; use super::SelectionSet; use super::SelectionValue; use crate::error::FederationError; -use crate::operation::HasSelectionKey; -use crate::schema::position::CompositeTypeDefinitionPosition; impl<'a> FieldSelectionValue<'a> { /// Merges the given field selections into this one. @@ -29,38 +28,43 @@ impl<'a> FieldSelectionValue<'a> { /// Returns an error if: /// - The parent type or schema of any selection does not match `self`'s. /// - Any selection does not select the same field position as `self`. - fn merge_into(&mut self, other: &FieldSelection) -> Result<(), FederationError> { + fn merge_into<'op>( + &mut self, + others: impl Iterator, + ) -> Result<(), FederationError> { let self_field = &self.get().field; - let mut selection_set = None; - let other_field = &other.field; - if other_field.schema != self_field.schema { - return Err(FederationError::internal( - "Cannot merge field selections from different schemas", - )); - } - if other_field.field_position != self_field.field_position { - return Err(FederationError::internal(format!( + let mut selection_sets = vec![]; + for other in others { + let other_field = &other.field; + if other_field.schema != self_field.schema { + return Err(FederationError::internal( + "Cannot merge field selections from different schemas", + )); + } + if other_field.field_position != self_field.field_position { + return Err(FederationError::internal(format!( "Cannot merge field selection for field \"{}\" into a field selection for field \"{}\"", other_field.field_position, self_field.field_position, ))); - } - if self.get().selection_set.is_some() { - let Some(other_selection_set) = &other.selection_set else { + } + if self.get().selection_set.is_some() { + let Some(other_selection_set) = &other.selection_set else { + return Err(FederationError::internal(format!( + "Field \"{}\" has composite type but not a selection set", + other_field.field_position, + ))); + }; + selection_sets.push(other_selection_set); + } else if other.selection_set.is_some() { return Err(FederationError::internal(format!( - "Field \"{}\" has composite type but not a selection set", + "Field \"{}\" has non-composite type but also has a selection set", other_field.field_position, ))); - }; - selection_set = Some(other_selection_set); - } else if other.selection_set.is_some() { - return Err(FederationError::internal(format!( - "Field \"{}\" has non-composite type but also has a selection set", - other_field.field_position, - ))); + } } if let Some(self_selection_set) = self.get_selection_set_mut() { - self_selection_set.merge_into(selection_set.into_iter())?; + self_selection_set.merge_into(selection_sets.into_iter())?; } Ok(()) } @@ -75,26 +79,35 @@ impl<'a> InlineFragmentSelectionValue<'a> { /// /// # Errors /// Returns an error if the parent type or schema of any selection does not match `self`'s. - fn merge_into(&mut self, other: &InlineFragmentSelection) -> Result<(), FederationError> { + fn merge_into<'op>( + &mut self, + others: impl Iterator, + ) -> Result<(), FederationError> { let self_inline_fragment = &self.get().inline_fragment; - let other_inline_fragment = &other.inline_fragment; - if other_inline_fragment.schema != self_inline_fragment.schema { - return Err(FederationError::internal( - "Cannot merge inline fragment from different schemas", - )); - } - if other_inline_fragment.parent_type_position != self_inline_fragment.parent_type_position { - return Err(FederationError::internal( - format!( - "Cannot merge inline fragment of parent type \"{}\" into an inline fragment of parent type \"{}\"", - other_inline_fragment.parent_type_position, - self_inline_fragment.parent_type_position, - ), - )); + let mut selection_sets = vec![]; + for other in others { + let other_inline_fragment = &other.inline_fragment; + if other_inline_fragment.schema != self_inline_fragment.schema { + return Err(FederationError::internal( + "Cannot merge inline fragment from different schemas", + )); + } + if other_inline_fragment.parent_type_position + != self_inline_fragment.parent_type_position + { + return Err(FederationError::internal( + format!( + "Cannot merge inline fragment of parent type \"{}\" into an inline fragment of parent type \"{}\"", + other_inline_fragment.parent_type_position, + self_inline_fragment.parent_type_position, + ), + )); + } + selection_sets.push(&other.selection_set); } - self.get_selection_set_mut() - .merge_into(std::iter::once(&other.selection_set)) + .merge_into(selection_sets.into_iter())?; + Ok(()) } } @@ -107,19 +120,24 @@ impl<'a> FragmentSpreadSelectionValue<'a> { /// /// # Errors /// Returns an error if the parent type or schema of any selection does not match `self`'s. - fn merge_into(&mut self, other: &FragmentSpreadSelection) -> Result<(), FederationError> { + fn merge_into<'op>( + &mut self, + others: impl Iterator, + ) -> Result<(), FederationError> { let self_fragment_spread = &self.get().spread; - let other_fragment_spread = &other.spread; - if other_fragment_spread.schema != self_fragment_spread.schema { - return Err(FederationError::internal( - "Cannot merge fragment spread from different schemas", - )); + for other in others { + let other_fragment_spread = &other.spread; + if other_fragment_spread.schema != self_fragment_spread.schema { + return Err(FederationError::internal( + "Cannot merge fragment spread from different schemas", + )); + } + // Nothing to do since the fragment spread is already part of the selection set. + // Fragment spreads are uniquely identified by fragment name and applied directives. + // Since there is already an entry for the same fragment spread, there is no point + // in attempting to merge its sub-selections, as the underlying entry should be + // exactly the same as the currently processed one. } - // Nothing to do since the fragment spread is already part of the selection set. - // Fragment spreads are uniquely identified by fragment name and applied directives. - // Since there is already an entry for the same fragment spread, there is no point - // in attempting to merge its sub-selections, as the underlying entry should be - // exactly the same as the currently processed one. Ok(()) } } @@ -155,65 +173,68 @@ impl SelectionSet { } selections_to_merge.extend(other.selections.values()); } - self.merge_selections_into(selections_to_merge.into_iter(), false) + self.merge_selections_into(selections_to_merge.into_iter()) } /// NOTE: This is a private API and should be used with care, use `add_selection` instead. /// /// A helper function for merging the given selections into this one. /// - /// The `do_fragment_inlining` flag enables a check to see if any inline fragments yielded from - /// `others` can be recursively merged into the selection set instead of just merging in the - /// fragment. This requires that the fragment has no directives and either has no type - /// condition or the type condition matches this selection set's type position. - /// /// # Errors /// Returns an error if the parent type or schema of any selection does not match `self`'s. /// /// Returns an error if any selection contains invalid GraphQL that prevents the merge. - #[allow(unreachable_code)] pub(super) fn merge_selections_into<'op>( &mut self, - mut others: impl Iterator, - do_fragment_inlining: bool, + others: impl Iterator, ) -> Result<(), FederationError> { - fn insert_selection( - target: &mut SelectionMap, - selection: &Selection, - ) -> Result<(), FederationError> { - match target.entry(selection.key()) { - selection_map::Entry::Vacant(vacant) => { - vacant.insert(selection.clone())?; - Ok(()) - } - selection_map::Entry::Occupied(mut entry) => match entry.get_mut() { - SelectionValue::Field(mut field) => { - let Selection::Field(other_field) = selection else { - return Err(FederationError::internal(format!( - "Field selection key for field \"{}\" references non-field selection", - field.get().field.field_position, - ))); + let mut fields = IndexMap::default(); + let mut fragment_spreads = IndexMap::default(); + let mut inline_fragments = IndexMap::default(); + let target = Arc::make_mut(&mut self.selections); + for other_selection in others { + let other_key = other_selection.key(); + match target.entry(other_key.clone()) { + selection_map::Entry::Occupied(existing) => match existing.get() { + Selection::Field(self_field_selection) => { + let Selection::Field(other_field_selection) = other_selection else { + return Err(FederationError::internal( + format!( + "Field selection key for field \"{}\" references non-field selection", + self_field_selection.field.field_position, + ), + )); }; - field.merge_into(other_field) + fields + .entry(other_key) + .or_insert_with(Vec::new) + .push(other_field_selection); } - SelectionValue::FragmentSpread(mut spread) => { - let Selection::FragmentSpread(other_spread) = selection else { + Selection::FragmentSpread(self_fragment_spread_selection) => { + let Selection::FragmentSpread(other_fragment_spread_selection) = + other_selection + else { return Err(FederationError::internal( format!( "Fragment spread selection key for fragment \"{}\" references non-field selection", - spread.get().spread.fragment_name, + self_fragment_spread_selection.spread.fragment_name, ), )); }; - spread.merge_into(other_spread) + fragment_spreads + .entry(other_key) + .or_insert_with(Vec::new) + .push(other_fragment_spread_selection); } - SelectionValue::InlineFragment(mut inline) => { - let Selection::InlineFragment(other_inline) = selection else { + Selection::InlineFragment(self_inline_fragment_selection) => { + let Selection::InlineFragment(other_inline_fragment_selection) = + other_selection + else { return Err(FederationError::internal( format!( "Inline fragment selection key under parent type \"{}\" {}references non-field selection", - inline.get().inline_fragment.parent_type_position, - inline.get().inline_fragment.type_condition_position.clone() + self_inline_fragment_selection.inline_fragment.parent_type_position, + self_inline_fragment_selection.inline_fragment.type_condition_position.clone() .map_or_else( String::new, |cond| format!("(type condition: {}) ", cond), @@ -221,36 +242,53 @@ impl SelectionSet { ), )); }; - inline.merge_into(other_inline) + inline_fragments + .entry(other_key) + .or_insert_with(Vec::new) + .push(other_inline_fragment_selection); } }, + selection_map::Entry::Vacant(vacant) => { + vacant.insert(other_selection.clone())?; + } } } - let target = Arc::make_mut(&mut self.selections); - - if do_fragment_inlining { - fn recurse_on_inline_fragment<'a>( - target: &mut SelectionMap, - type_pos: &CompositeTypeDefinitionPosition, - mut others: impl Iterator, - ) -> Result<(), FederationError> { - others.try_for_each(|selection| match selection { - Selection::InlineFragment(inline) if inline.is_unnecessary(type_pos) => { - recurse_on_inline_fragment( - target, - type_pos, - inline.selection_set.selections.values(), - ) + for (key, self_selection) in target.iter_mut() { + match self_selection { + SelectionValue::Field(mut self_field_selection) => { + if let Some(other_field_selections) = fields.shift_remove(key) { + self_field_selection.merge_into( + other_field_selections.iter().map(|selection| &***selection), + )?; + } + } + SelectionValue::FragmentSpread(mut self_fragment_spread_selection) => { + if let Some(other_fragment_spread_selections) = + fragment_spreads.shift_remove(key) + { + self_fragment_spread_selection.merge_into( + other_fragment_spread_selections + .iter() + .map(|selection| &***selection), + )?; } - selection => insert_selection(target, selection), - }) + } + SelectionValue::InlineFragment(mut self_inline_fragment_selection) => { + if let Some(other_inline_fragment_selections) = + inline_fragments.shift_remove(key) + { + self_inline_fragment_selection.merge_into( + other_inline_fragment_selections + .iter() + .map(|selection| &***selection), + )?; + } + } } - - recurse_on_inline_fragment(target, &self.type_position, others) - } else { - others.try_for_each(|selection| insert_selection(target, selection)) } + + Ok(()) } /// Inserts a `Selection` into the inner map. Should a selection with the same key already @@ -267,14 +305,13 @@ impl SelectionSet { pub(crate) fn add_local_selection( &mut self, selection: &Selection, - do_fragment_inlining: bool, ) -> Result<(), FederationError> { debug_assert_eq!( &self.schema, selection.schema(), "In order to add selection it needs to point to the same schema" ); - self.merge_selections_into(std::iter::once(selection), do_fragment_inlining) + self.merge_selections_into(std::iter::once(selection)) } /// Inserts a `SelectionSet` into the inner map. Should any sub selection with the same key already diff --git a/apollo-federation/src/operation/mod.rs b/apollo-federation/src/operation/mod.rs index fdbfff205b..029294e9ac 100644 --- a/apollo-federation/src/operation/mod.rs +++ b/apollo-federation/src/operation/mod.rs @@ -25,6 +25,7 @@ use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::executable; use apollo_compiler::name; +use apollo_compiler::schema::Directive; use apollo_compiler::validation::Valid; use apollo_compiler::Name; use apollo_compiler::Node; @@ -35,6 +36,8 @@ use crate::compat::coerce_executable_values; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::error::SingleFederationError::Internal; +use crate::link::graphql_definition::BooleanOrVariable; +use crate::link::graphql_definition::DeferDirectiveArguments; use crate::query_graph::graph_path::OpPathElement; use crate::query_plan::conditions::Conditions; use crate::query_plan::FetchDataKeyRenamer; @@ -197,13 +200,6 @@ pub struct Operation { pub(crate) named_fragments: NamedFragments, } -pub(crate) struct NormalizedDefer { - pub operation: Operation, - pub has_defers: bool, - pub assigned_defer_labels: IndexSet, - pub defer_conditions: IndexMap>, -} - impl Operation { /// Parse an operation from a source string. #[cfg(any(test, doc))] @@ -242,49 +238,6 @@ impl Operation { named_fragments, }) } - - // PORT_NOTE(@goto-bus-stop): It might make sense for the returned data structure to *be* the - // `DeferNormalizer` from the JS side - pub(crate) fn with_normalized_defer(self) -> NormalizedDefer { - NormalizedDefer { - operation: self, - has_defers: false, - assigned_defer_labels: IndexSet::default(), - defer_conditions: IndexMap::default(), - } - // TODO(@TylerBloom): Once defer is implement, the above statement needs to be replaced - // with the commented-out one below. This is part of FED-95 - /* - if self.has_defer() { - todo!("@defer not implemented"); - } else { - NormalizedDefer { - operation: self, - has_defers: false, - assigned_defer_labels: IndexSet::default(), - defer_conditions: IndexMap::default(), - } - } - */ - } - - fn has_defer(&self) -> bool { - self.selection_set.has_defer() - || self - .named_fragments - .fragments - .values() - .any(|f| f.has_defer()) - } - - /// Removes the @defer directive from all selections without removing that selection. - pub(crate) fn without_defer(mut self) -> Self { - if self.has_defer() { - self.selection_set.without_defer(); - } - debug_assert!(!self.has_defer()); - self - } } /// An analogue of the apollo-compiler type `SelectionSet` with these changes: @@ -533,11 +486,11 @@ mod selection_map { } } - pub(super) fn get_directives_mut(&mut self) -> &mut DirectiveList { + pub(super) fn directives(&self) -> &'_ DirectiveList { match self { - Self::Field(field) => field.get_directives_mut(), - Self::FragmentSpread(spread) => spread.get_directives_mut(), - Self::InlineFragment(inline) => inline.get_directives_mut(), + Self::Field(field) => &field.get().field.directives, + Self::FragmentSpread(frag) => &frag.get().spread.directives, + Self::InlineFragment(frag) => &frag.get().inline_fragment.directives, } } @@ -566,10 +519,6 @@ mod selection_map { Arc::make_mut(self.0).field.sibling_typename_mut() } - pub(super) fn get_directives_mut(&mut self) -> &mut DirectiveList { - Arc::make_mut(self.0).field.directives_mut() - } - pub(crate) fn get_selection_set_mut(&mut self) -> &mut Option { &mut Arc::make_mut(self.0).selection_set } @@ -583,10 +532,6 @@ mod selection_map { Self(fragment_spread_selection) } - pub(super) fn get_directives_mut(&mut self) -> &mut DirectiveList { - Arc::make_mut(self.0).spread.directives_mut() - } - pub(crate) fn get_selection_set_mut(&mut self) -> &mut SelectionSet { &mut Arc::make_mut(self.0).selection_set } @@ -608,10 +553,6 @@ mod selection_map { self.0 } - pub(super) fn get_directives_mut(&mut self) -> &mut DirectiveList { - Arc::make_mut(self.0).inline_fragment.directives_mut() - } - pub(crate) fn get_selection_set_mut(&mut self) -> &mut SelectionSet { &mut Arc::make_mut(self.0).selection_set } @@ -673,8 +614,8 @@ mod selection_map { if *self.key() != value.key() { return Err(Internal { message: format!( - "Key mismatch when inserting selection `{value}` into vacant entry. Expected {:?}, found {:?}", - self.key(), value.key() + "Key mismatch when inserting selection {} into vacant entry ", + value ), } .into()); @@ -691,6 +632,15 @@ mod selection_map { as IntoIterator>::into_iter(self.0) } } + + impl<'a> IntoIterator for &'a SelectionMap { + type Item = <&'a IndexMap as IntoIterator>::Item; + type IntoIter = <&'a IndexMap as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } + } } pub(crate) use selection_map::FieldSelectionValue; @@ -789,7 +739,6 @@ impl Selection { pub(crate) fn from_element( element: OpPathElement, sub_selections: Option, - unnecessary_directives: Option<&HashSet>>, ) -> Result { // PORT_NOTE: This is TODO item is copied from the JS `selectionOfElement` function. // TODO: validate that the subSelection is ok for the element @@ -801,21 +750,7 @@ impl Selection { "unexpected inline fragment without sub-selections", )); }; - if let Some(unnecessary_directives) = unnecessary_directives { - let directives = inline_fragment - .directives - .iter() - .filter(|dir| !unnecessary_directives.contains(dir.as_ref())) - .cloned() - .collect::(); - Ok(InlineFragmentSelection::new( - inline_fragment.with_updated_directives(directives), - sub_selections, - ) - .into()) - } else { - Ok(InlineFragmentSelection::new(inline_fragment, sub_selections).into()) - } + Ok(InlineFragmentSelection::new(inline_fragment, sub_selections).into()) } } } @@ -938,18 +873,6 @@ impl Selection { } } - pub(crate) fn has_defer(&self) -> bool { - match self { - Selection::Field(field_selection) => field_selection.has_defer(), - Selection::FragmentSpread(fragment_spread_selection) => { - fragment_spread_selection.has_defer() - } - Selection::InlineFragment(inline_fragment_selection) => { - inline_fragment_selection.has_defer() - } - } - } - pub(crate) fn with_updated_selection_set( &self, selection_set: Option, @@ -1101,10 +1024,6 @@ impl Fragment { )?, }) } - - fn has_defer(&self) -> bool { - self.selection_set.has_defer() - } } mod field_selection { @@ -1545,10 +1464,6 @@ pub(crate) use fragment_spread_selection::FragmentSpreadData; pub(crate) use fragment_spread_selection::FragmentSpreadSelection; impl FragmentSpreadSelection { - pub(crate) fn has_defer(&self) -> bool { - self.spread.directives.has("defer") || self.selection_set.has_defer() - } - /// Copies fragment spread selection and assigns it a new unique selection ID. pub(crate) fn with_unique_id(&self) -> Self { let mut data = self.spread.data().clone(); @@ -1686,6 +1601,7 @@ mod inline_fragment_selection { selection_set: self.selection_set.clone(), } } + pub(crate) fn with_updated_directives_and_selection_set( &self, directives: impl Into, @@ -1931,7 +1847,7 @@ impl SelectionSet { .flat_map(SelectionSet::split_top_level_fields) .filter_map(move |set| { let parent_type = ele.parent_type_position(); - Selection::from_element(ele.clone(), Some(set), None) + Selection::from_element(ele.clone(), Some(set)) .ok() .map(|sel| SelectionSet::from_selection(parent_type, sel)) }), @@ -2051,7 +1967,7 @@ impl SelectionSet { type_position, selections: Arc::new(SelectionMap::new()), }; - merged.merge_selections_into(normalized_selections.iter(), false)?; + merged.merge_selections_into(normalized_selections.iter())?; Ok(merged) } @@ -2148,7 +2064,7 @@ impl SelectionSet { type_position: self.type_position.clone(), selections: Arc::new(SelectionMap::new()), }; - expanded.merge_selections_into(expanded_selections.iter(), false)?; + expanded.merge_selections_into(expanded_selections.iter())?; Ok(expanded) } @@ -2522,7 +2438,7 @@ impl SelectionSet { sibling_typename.alias().cloned(), ); let typename_selection = - Selection::from_element(field_element.into(), /*subselection*/ None, None)?; + Selection::from_element(field_element.into(), /*subselection*/ None)?; Ok([typename_selection, updated].into_iter().collect()) }) } @@ -2626,16 +2542,6 @@ impl SelectionSet { &mut self, path: &[Arc], selection_set: Option<&Arc>, - ) -> Result<(), FederationError> { - let mut unnecessary_directives = HashSet::default(); - self.add_at_path_inner(path, selection_set, &mut unnecessary_directives) - } - - fn add_at_path_inner( - &mut self, - path: &[Arc], - selection_set: Option<&Arc>, - unnecessary_directives: &mut HashSet>, ) -> Result<(), FederationError> { // PORT_NOTE: This method was ported from the JS class `SelectionSetUpdates`. Unlike the // JS code, this mutates the selection set map in-place. @@ -2646,39 +2552,28 @@ impl SelectionSet { let Some(sub_selection_type) = element.sub_selection_type_position()? else { return Err(FederationError::internal("unexpected error: add_at_path encountered a field that is not of a composite type".to_string())); }; - let target = Arc::make_mut(&mut self.selections); - let mut selection = match target.get_mut(&ele.key()) { - Some(selection) => selection, - None => { - let selection = Selection::from_element( + let mut selection = Arc::make_mut(&mut self.selections) + .entry(ele.key()) + .or_insert(|| { + Selection::from_element( element, // We immediately add a selection afterward to make this selection set // valid. Some(SelectionSet::empty(self.schema.clone(), sub_selection_type)), - Some(&*unnecessary_directives), - )?; - target.entry(selection.key()).or_insert(|| Ok(selection))? - } - }; - unnecessary_directives.extend( - selection - .get_directives_mut() - .iter() - .filter(|d| d.name == "include" || d.name == "skip") - .cloned(), - ); + ) + })?; match &mut selection { SelectionValue::Field(field) => match field.get_selection_set_mut() { - Some(sub_selection) => sub_selection.add_at_path_inner(path, selection_set, unnecessary_directives), + Some(sub_selection) => sub_selection.add_at_path(path, selection_set)?, None => return Err(FederationError::internal("add_at_path encountered a field without a subselection which should never happen".to_string())), }, SelectionValue::InlineFragment(fragment) => fragment .get_selection_set_mut() - .add_at_path_inner(path, selection_set, unnecessary_directives), + .add_at_path(path, selection_set)?, SelectionValue::FragmentSpread(_fragment) => { - return Err(FederationError::internal("add_at_path encountered a named fragment spread which should never happen".to_string())) + return Err(FederationError::internal("add_at_path encountered a named fragment spread which should never happen".to_string())); } - }?; + }; } // If we have no sub-path, we can add the selection. Some((ele, &[])) => { @@ -2697,29 +2592,35 @@ impl SelectionSet { return Ok(()); } else { // add leaf - let selection = - Selection::from_element(element, None, Some(&*unnecessary_directives))?; - self.add_local_selection(&selection, true)? + let selection = Selection::from_element(element, None)?; + self.add_local_selection(&selection)? } } else { + let sub_selection_type_pos = element.sub_selection_type_position()?.ok_or_else(|| { + FederationError::internal("unexpected: Element has a selection set with non-composite base type") + })?; let selection_set = selection_set .map(|selection_set| { - selection_set.rebase_on( - &element.sub_selection_type_position()?.ok_or_else(|| { - FederationError::internal("unexpected: Element has a selection set with non-composite base type") - })?, - &NamedFragments::default(), + let selections = selection_set.without_unnecessary_fragments( + &sub_selection_type_pos, &self.schema, - ) + ); + let mut selection_set = SelectionSet::empty( + self.schema.clone(), + sub_selection_type_pos.clone(), + ); + for selection in selections.iter() { + selection_set.add_local_selection(&selection.rebase_on( + &sub_selection_type_pos, + &NamedFragments::default(), + &self.schema, + )?)?; + } + Ok::<_, FederationError>(selection_set) }) - .transpose()? - .map(|selection_set| selection_set.without_unnecessary_fragments()); - let selection = Selection::from_element( - element, - selection_set, - Some(&*unnecessary_directives), - )?; - self.add_local_selection(&selection, true)?; + .transpose()?; + let selection = Selection::from_element(element, selection_set)?; + self.add_local_selection(&selection)? } } // If we don't have any path, we rebase and merge in the given sub selections at the root. @@ -2732,23 +2633,6 @@ impl SelectionSet { Ok(()) } - /// Removes the @defer directive from all selections without removing that selection. - fn without_defer(&mut self) { - for (_key, mut selection) in Arc::make_mut(&mut self.selections).iter_mut() { - // TODO(@goto-bus-stop): doing this changes the key of the selection! - // We have to rebuild the selection map. - selection.get_directives_mut().remove_one("defer"); - if let Some(set) = selection.get_selection_set_mut() { - set.without_defer(); - } - } - debug_assert!(!self.has_defer()); - } - - fn has_defer(&self) -> bool { - self.selections.values().any(|s| s.has_defer()) - } - // - `self` must be fragment-spread-free. pub(crate) fn add_aliases_for_non_merging_fields( &self, @@ -2931,39 +2815,49 @@ impl SelectionSet { } } + /// Using path-based updates along with selection sets may result in some inefficiencies. + /// Specifically, we may end up with some unnecessary top-level inline fragment selections, i.e. + /// fragments without any directives and with the type condition equal to (or a supertype of) + /// the parent type of the fragment. This method inlines those unnecessary top-level fragments. + /// /// JS PORT NOTE: In Rust implementation we are doing the selection set updates in-place whereas /// JS code was pooling the updates and only apply those when building the final selection set. /// See `makeSelectionSet` method for details. - /// - /// Manipulating selection sets may result in some inefficiencies. As a result we may end up with - /// some unnecessary top level inline fragment selections, i.e. fragments without any directives - /// and with the type condition same as the parent type that should be inlined. - /// - /// This method inlines those unnecessary top level fragments only. While the JS code was applying - /// this logic recursively, since we are manipulating selections sets in-place we only need to - /// apply this normalization at the top level. - fn without_unnecessary_fragments(&self) -> SelectionSet { - let parent_type = &self.type_position; - let mut final_selections = SelectionMap::new(); - for selection in self.selections.values() { - match selection { - Selection::InlineFragment(inline_fragment) => { - if inline_fragment.is_unnecessary(parent_type) { - final_selections.extend_ref(&inline_fragment.selection_set.selections); - } else { - final_selections.insert(selection.clone()); + fn without_unnecessary_fragments( + &self, + parent_type: &CompositeTypeDefinitionPosition, + schema: &ValidFederationSchema, + ) -> Vec { + let mut final_selections = vec![]; + fn process_selection_set( + selection_set: &SelectionSet, + final_selections: &mut Vec, + parent_type: &CompositeTypeDefinitionPosition, + schema: &ValidFederationSchema, + ) { + for selection in selection_set.selections.values() { + match selection { + Selection::InlineFragment(inline_fragment) => { + if inline_fragment.is_unnecessary(parent_type, schema) { + process_selection_set( + &inline_fragment.selection_set, + final_selections, + parent_type, + schema, + ); + } else { + final_selections.push(selection.clone()); + } + } + _ => { + final_selections.push(selection.clone()); } - } - _ => { - final_selections.insert(selection.clone()); } } } - SelectionSet { - schema: self.schema.clone(), - type_position: parent_type.clone(), - selections: Arc::new(final_selections), - } + process_selection_set(self, &mut final_selections, parent_type, schema); + + final_selections } pub(crate) fn iter(&self) -> impl Iterator { @@ -2994,6 +2888,15 @@ impl IntoIterator for SelectionSet { } } +impl<'a> IntoIterator for &'a SelectionSet { + type Item = <&'a IndexMap as IntoIterator>::Item; + type IntoIter = <&'a IndexMap as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.selections.as_ref().into_iter() + } +} + pub(crate) struct FieldSelectionsIter<'sel> { stack: Vec>, } @@ -3285,10 +3188,6 @@ impl FieldSelection { } } - pub(crate) fn has_defer(&self) -> bool { - self.field.has_defer() || self.selection_set.as_ref().is_some_and(|s| s.has_defer()) - } - pub(crate) fn any_element( &self, predicate: &mut impl FnMut(OpPathElement) -> Result, @@ -3306,11 +3205,6 @@ impl FieldSelection { } impl Field { - pub(crate) fn has_defer(&self) -> bool { - // @defer cannot be on field at the moment - false - } - pub(crate) fn parent_type_position(&self) -> CompositeTypeDefinitionPosition { self.field_position.parent() } @@ -3455,25 +3349,26 @@ impl InlineFragmentSelection { .unwrap_or(&self.inline_fragment.parent_type_position) } - pub(crate) fn has_defer(&self) -> bool { - self.inline_fragment.directives.has("defer") - || self - .selection_set - .selections - .values() - .any(|s| s.has_defer()) - } - /// Returns true if this inline fragment selection is "unnecessary" and should be inlined. /// /// Fragment is unnecessary if following are true: /// * it has no applied directives - /// * has no type condition OR type condition is same as passed in `maybe_parent` - fn is_unnecessary(&self, maybe_parent: &CompositeTypeDefinitionPosition) -> bool { - let inline_fragment_type_condition = self.inline_fragment.type_condition_position.clone(); - self.inline_fragment.directives.is_empty() - && (inline_fragment_type_condition.is_none() - || inline_fragment_type_condition.is_some_and(|t| t == *maybe_parent)) + /// * has no type condition OR type condition is equal to (or a supertype of) `parent` + fn is_unnecessary( + &self, + parent: &CompositeTypeDefinitionPosition, + schema: &ValidFederationSchema, + ) -> bool { + if !self.inline_fragment.directives.is_empty() { + return false; + } + let Some(type_condition) = &self.inline_fragment.type_condition_position else { + return true; + }; + type_condition.type_name() == parent.type_name() + || schema + .schema() + .is_subtype(type_condition.type_name(), parent.type_name()) } pub(crate) fn any_element( @@ -3668,6 +3563,456 @@ impl NamedFragments { } } +// @defer handling: removing and normalization + +const DEFER_DIRECTIVE_NAME: Name = name!("defer"); +const DEFER_LABEL_ARGUMENT_NAME: Name = name!("label"); +const DEFER_IF_ARGUMENT_NAME: Name = name!("if"); + +pub(crate) struct NormalizedDefer { + /// The operation modified to normalize @defer applications. + pub operation: Operation, + /// True if the operation contains any @defer applications. + pub has_defers: bool, + /// `@defer(label:)` values assigned by normalization. + pub assigned_defer_labels: IndexSet, + /// Map of variable conditions to the @defer labels depending on those conditions. + pub defer_conditions: IndexMap>, +} + +struct DeferNormalizer { + used_labels: HashSet, + assigned_labels: IndexSet, + conditions: IndexMap>, + label_offset: usize, +} + +impl DeferNormalizer { + fn new(selection_set: &SelectionSet) -> Result { + let mut digest = Self { + used_labels: HashSet::default(), + label_offset: 0, + assigned_labels: IndexSet::default(), + conditions: IndexMap::default(), + }; + let mut stack = selection_set + .into_iter() + .map(|(_, sel)| sel) + .collect::>(); + while let Some(selection) = stack.pop() { + if let Selection::InlineFragment(inline) = selection { + if let Some(args) = inline.inline_fragment.data().defer_directive_arguments()? { + let DeferDirectiveArguments { label, if_: _ } = args; + if let Some(label) = label { + digest.used_labels.insert(label); + } + } + } + stack.extend( + selection + .selection_set() + .into_iter() + .flatten() + .map(|(_, sel)| sel), + ); + } + Ok(digest) + } + + fn get_label(&mut self) -> String { + loop { + let digest = format!("qp__{}", self.label_offset); + self.label_offset += 1; + if !self.used_labels.contains(&digest) { + self.assigned_labels.insert(digest.clone()); + return digest; + } + } + } + + fn register_condition(&mut self, label: String, cond: Name) { + self.conditions.entry(cond).or_default().insert(label); + } +} + +#[derive(Debug, Clone, Copy)] +enum DeferFilter<'a> { + All, + Labels(&'a IndexSet), +} + +impl DeferFilter<'_> { + fn remove_defer(&self, directive_list: &mut DirectiveList, schema: &apollo_compiler::Schema) { + match self { + Self::All => { + directive_list.remove_one(&DEFER_DIRECTIVE_NAME); + } + Self::Labels(set) => { + let label = directive_list + .get(&DEFER_DIRECTIVE_NAME) + .and_then(|directive| { + directive + .argument_by_name(&DEFER_LABEL_ARGUMENT_NAME, schema) + .ok() + }) + .and_then(|arg| arg.as_str()); + if label.is_some_and(|label| set.contains(label)) { + directive_list.remove_one(&DEFER_DIRECTIVE_NAME); + } + } + } + } +} + +impl Fragment { + /// Returns true if the fragment's selection set contains the @defer directive. + fn has_defer(&self) -> bool { + self.selection_set.has_defer() + } + + fn without_defer( + &self, + filter: DeferFilter<'_>, + named_fragments: &NamedFragments, + ) -> Result { + let selection_set = self.selection_set.without_defer(filter, named_fragments)?; + Ok(Fragment { + schema: self.schema.clone(), + name: self.name.clone(), + type_condition_position: self.type_condition_position.clone(), + directives: self.directives.clone(), + selection_set, + }) + } +} + +impl NamedFragments { + /// Returns true if any fragment uses the @defer directive. + fn has_defer(&self) -> bool { + self.iter().any(|fragment| fragment.has_defer()) + } + + /// Creates new fragment definitions with the @defer directive removed. + fn without_defer(&self, filter: DeferFilter<'_>) -> Result { + let mut new_fragments = NamedFragments { + fragments: Default::default(), + }; + // The iteration is in dependency order: when we iterate a fragment A that depends on + // fragment B, we know that we have already processed fragment B. + // This implies that all references to other fragments will already be part of + // `new_fragments`. Note that we must process all fragments that depend on each other, even + // if a fragment doesn't actually use @defer itself, to make sure that the `.selection_set` + // values on each selection are up to date. + for fragment in self.iter() { + let fragment = fragment.without_defer(filter, &new_fragments)?; + new_fragments.insert(fragment); + } + Ok(new_fragments) + } +} + +impl FieldSelection { + /// Returns true if the selection or any of its subselections uses the @defer directive. + fn has_defer(&self) -> bool { + // Fields don't have @defer, so we only check the subselection. + self.selection_set.as_ref().is_some_and(|s| s.has_defer()) + } +} + +impl FragmentSpread { + /// Returns true if the fragment spread has a @defer directive. + fn has_defer(&self) -> bool { + self.directives.has(&DEFER_DIRECTIVE_NAME) + } + + fn without_defer(&self, filter: DeferFilter<'_>) -> Result { + let mut data = self.data().clone(); + filter.remove_defer(&mut data.directives, data.schema.schema()); + Ok(Self::new(data)) + } +} + +impl FragmentSpreadSelection { + fn has_defer(&self) -> bool { + self.spread.has_defer() || self.selection_set.has_defer() + } +} + +impl InlineFragment { + /// Returns true if the fragment has a @defer directive. + fn has_defer(&self) -> bool { + self.directives.has(&DEFER_DIRECTIVE_NAME) + } + + fn without_defer(&self, filter: DeferFilter<'_>) -> Result { + let mut data = self.data().clone(); + filter.remove_defer(&mut data.directives, data.schema.schema()); + Ok(Self::new(data)) + } +} + +impl InlineFragmentSelection { + /// Returns true if the selection or any of its subselections uses the @defer directive. + fn has_defer(&self) -> bool { + self.inline_fragment.has_defer() + || self + .selection_set + .selections + .values() + .any(|s| s.has_defer()) + } + + fn normalize_defer(self, normalizer: &mut DeferNormalizer) -> Result { + // This should always be `Some` + let Some(args) = self.inline_fragment.defer_directive_arguments()? else { + return Ok(self); + }; + + let mut remove_defer = false; + let mut args_copy = args.clone(); + if let Some(BooleanOrVariable::Boolean(b)) = &args.if_ { + if *b { + args_copy.if_ = None; + } else { + remove_defer = true; + } + } + + if args_copy.label.is_none() { + args_copy.label = Some(normalizer.get_label()); + } + + if remove_defer { + let directives: DirectiveList = self + .inline_fragment + .directives + .iter() + .filter(|dir| dir.name != "defer") + .cloned() + .collect(); + return Ok(self.with_updated_directives(directives)); + } + + // NOTE: If this is `Some`, it will be a variable. + if let Some(BooleanOrVariable::Variable(cond)) = args_copy.if_.clone() { + normalizer.register_condition(args_copy.label.clone().unwrap(), cond); + } + + if args_copy == args { + Ok(self) + } else { + let directives: DirectiveList = self + .inline_fragment + .directives + .iter() + .map(|dir| { + if dir.name == "defer" { + let mut dir: Directive = (**dir).clone(); + dir.arguments.retain(|arg| { + ![DEFER_LABEL_ARGUMENT_NAME, DEFER_IF_ARGUMENT_NAME].contains(&arg.name) + }); + dir.arguments.push( + (DEFER_LABEL_ARGUMENT_NAME, args_copy.label.clone().unwrap()).into(), + ); + if let Some(cond) = args_copy.if_.clone() { + dir.arguments.push((DEFER_IF_ARGUMENT_NAME, cond).into()); + } + Node::new(dir) + } else { + dir.clone() + } + }) + .collect(); + Ok(self.with_updated_directives(directives)) + } + } +} + +impl Selection { + /// Returns true if the selection or any of its subselections uses the @defer directive. + pub(crate) fn has_defer(&self) -> bool { + match self { + Selection::Field(field_selection) => field_selection.has_defer(), + Selection::FragmentSpread(fragment_spread_selection) => { + fragment_spread_selection.has_defer() + } + Selection::InlineFragment(inline_fragment_selection) => { + inline_fragment_selection.has_defer() + } + } + } + + fn without_defer( + &self, + filter: DeferFilter<'_>, + named_fragments: &NamedFragments, + ) -> Result { + match self { + Selection::Field(field) => { + let Some(selection_set) = field + .selection_set + .as_ref() + .filter(|selection_set| selection_set.has_defer()) + else { + return Ok(Selection::Field(Arc::clone(field))); + }; + + Ok(field + .with_updated_selection_set(Some( + selection_set.without_defer(filter, named_fragments)?, + )) + .into()) + } + Selection::FragmentSpread(frag) => { + let spread = frag.spread.without_defer(filter)?; + Ok(FragmentSpreadSelection::new(spread, named_fragments)?.into()) + } + Selection::InlineFragment(frag) => { + let inline_fragment = frag.inline_fragment.without_defer(filter)?; + let selection_set = frag.selection_set.without_defer(filter, named_fragments)?; + Ok(InlineFragmentSelection::new(inline_fragment, selection_set).into()) + } + } + } + + fn normalize_defer(self, normalizer: &mut DeferNormalizer) -> Result { + match self { + Selection::Field(field) => Ok(Self::Field(Arc::new( + field.with_updated_selection_set( + field + .selection_set + .clone() + .map(|set| set.normalize_defer(normalizer)) + .transpose()?, + ), + ))), + Selection::FragmentSpread(_spread) => { + Err(FederationError::internal("unexpected fragment spread")) + } + Selection::InlineFragment(inline) => inline + .with_updated_selection_set( + inline.selection_set.clone().normalize_defer(normalizer)?, + ) + .normalize_defer(normalizer) + .map(|inline| Self::InlineFragment(Arc::new(inline))), + } + } +} + +impl SelectionSet { + /// Create a new selection set without @defer directive applications. + fn without_defer( + &self, + filter: DeferFilter<'_>, + named_fragments: &NamedFragments, + ) -> Result { + let mut without_defer = + SelectionSet::empty(self.schema.clone(), self.type_position.clone()); + for selection in self.selections.values() { + without_defer + .add_local_selection(&selection.without_defer(filter, named_fragments)?)?; + } + Ok(without_defer) + } + + fn has_defer(&self) -> bool { + self.selections.values().any(|s| s.has_defer()) + } + + fn normalize_defer(self, normalizer: &mut DeferNormalizer) -> Result { + let Self { + schema, + type_position, + selections, + } = self; + Arc::unwrap_or_clone(selections) + .into_iter() + .map(|(_, sel)| sel.normalize_defer(normalizer)) + .try_collect() + .map(|selections| Self { + schema, + type_position, + selections: Arc::new(selections), + }) + } +} + +impl Operation { + fn has_defer(&self) -> bool { + self.selection_set.has_defer() + || self + .named_fragments + .fragments + .values() + .any(|f| f.has_defer()) + } + + /// Create a new operation without @defer directive applications. + pub(crate) fn without_defer(mut self) -> Result { + if self.has_defer() { + let named_fragments = self.named_fragments.without_defer(DeferFilter::All)?; + self.selection_set = self + .selection_set + .without_defer(DeferFilter::All, &named_fragments)?; + self.named_fragments = named_fragments; + } + debug_assert!(!self.has_defer()); + Ok(self) + } + + /// Create a new operation without specific @defer(label:) directive applications. + pub(crate) fn reduce_defer( + mut self, + labels: &IndexSet, + ) -> Result { + if self.has_defer() { + let named_fragments = self + .named_fragments + .without_defer(DeferFilter::Labels(labels))?; + self.selection_set = self + .selection_set + .without_defer(DeferFilter::Labels(labels), &named_fragments)?; + self.named_fragments = named_fragments; + } + Ok(self) + } + + /// Returns this operation but modified to "normalize" all the @defer applications. + /// + /// "Normalized" in this context means that all the `@defer` application in the resulting + /// operation will: + /// - have a (unique) label. Which implies that this method generates a label for any `@defer` + /// not having a label. + /// - have a non-trivial `if` condition, if any. By non-trivial, we mean that the condition + /// will be a variable and not an hard-coded `true` or `false`. To do this, this method will + /// remove the condition of any `@defer` that has `if: true`, and will completely remove any + /// `@defer` application that has `if: false`. + /// + /// Defer normalization does not support named fragment definitions, so it must only be called + /// if the operation had its fragments expanded. In effect, it means that this method may + /// modify the operation in a way that prevents fragments from being reused in + /// `.reuse_fragments()`. + pub(crate) fn with_normalized_defer(mut self) -> Result { + if self.has_defer() { + let mut normalizer = DeferNormalizer::new(&self.selection_set)?; + self.selection_set = self.selection_set.normalize_defer(&mut normalizer)?; + Ok(NormalizedDefer { + operation: self, + has_defers: true, + assigned_defer_labels: normalizer.assigned_labels, + defer_conditions: normalizer.conditions, + }) + } else { + Ok(NormalizedDefer { + operation: self, + has_defers: false, + assigned_defer_labels: IndexSet::default(), + defer_conditions: IndexMap::default(), + }) + } + } +} + // Collect fragment usages from operation types. impl Selection { diff --git a/apollo-federation/src/operation/optimize.rs b/apollo-federation/src/operation/optimize.rs index ce2802bf4e..79fe71c8ef 100644 --- a/apollo-federation/src/operation/optimize.rs +++ b/apollo-federation/src/operation/optimize.rs @@ -1007,7 +1007,7 @@ impl SelectionSet { &fragment, /*directives*/ &Default::default(), ); - optimized.add_local_selection(&fragment_selection.into(), false)?; + optimized.add_local_selection(&fragment_selection.into())?; } optimized.add_local_selection_set(¬_covered_so_far)?; @@ -1697,21 +1697,17 @@ impl FragmentGenerator { self.visit_selection_set(selection_set)?; } new_selection_set - .add_local_selection(&Selection::Field(Arc::clone(field.get())), false)?; + .add_local_selection(&Selection::Field(Arc::clone(field.get())))?; } SelectionValue::FragmentSpread(frag) => { - new_selection_set.add_local_selection( - &Selection::FragmentSpread(Arc::clone(frag.get())), - false, - )?; + new_selection_set + .add_local_selection(&Selection::FragmentSpread(Arc::clone(frag.get())))?; } SelectionValue::InlineFragment(frag) if !Self::is_worth_using(&frag.get().selection_set) => { - new_selection_set.add_local_selection( - &Selection::InlineFragment(Arc::clone(frag.get())), - false, - )?; + new_selection_set + .add_local_selection(&Selection::InlineFragment(Arc::clone(frag.get())))?; } SelectionValue::InlineFragment(mut candidate) => { self.visit_selection_set(candidate.get_selection_set_mut())?; @@ -1729,10 +1725,9 @@ impl FragmentGenerator { // we can't just transfer them to the generated fragment spread, // so we have to keep this inline fragment. let Ok(skip_include) = skip_include else { - new_selection_set.add_local_selection( - &Selection::InlineFragment(Arc::clone(candidate.get())), - false, - )?; + new_selection_set.add_local_selection(&Selection::InlineFragment( + Arc::clone(candidate.get()), + ))?; continue; }; @@ -1741,10 +1736,9 @@ impl FragmentGenerator { // there's any directives on it. This code duplicates the body from the // previous condition so it's very easy to remove when we're ready :) if !skip_include.is_empty() { - new_selection_set.add_local_selection( - &Selection::InlineFragment(Arc::clone(candidate.get())), - false, - )?; + new_selection_set.add_local_selection(&Selection::InlineFragment( + Arc::clone(candidate.get()), + ))?; continue; } @@ -1769,8 +1763,8 @@ impl FragmentGenerator { }); self.fragments.get(&name).unwrap() }; - new_selection_set.add_local_selection( - &Selection::from(FragmentSpreadSelection { + new_selection_set.add_local_selection(&Selection::from( + FragmentSpreadSelection { spread: FragmentSpread::new(FragmentSpreadData { schema: selection_set.schema.clone(), fragment_name: existing.name.clone(), @@ -1780,9 +1774,8 @@ impl FragmentGenerator { selection_id: crate::operation::SelectionId::new(), }), selection_set: existing.selection_set.clone(), - }), - false, - )?; + }, + ))?; } } } @@ -2288,12 +2281,12 @@ mod tests { type Query { t: T } - + type T { a: A b: Int } - + type A { x: String y: String @@ -2319,14 +2312,14 @@ mod tests { x y } - + fragment FT on T { a { __typename ...FA } } - + query { t { ...FT @@ -2353,19 +2346,19 @@ mod tests { type Query { t: T } - + type T { a: String b: B c: Int d: D } - + type B { x: String y: String } - + type D { m: String n: String @@ -2383,7 +2376,7 @@ mod tests { m } } - + { t { ...FragT @@ -2422,23 +2415,23 @@ mod tests { type Query { i: I } - + interface I { a: String } - + type T implements I { a: String b: B c: Int d: D } - + type B { x: String y: String } - + type D { m: String n: String @@ -2456,7 +2449,7 @@ mod tests { m } } - + { i { ... on T { @@ -2496,19 +2489,19 @@ mod tests { type Query { t: T } - + type T { a: String b: B c: Int d: D } - + type B { x: String y: String } - + type D { m: String n: String @@ -2534,7 +2527,7 @@ mod tests { m } } - + fragment Frag2 on T { a b { @@ -2546,7 +2539,7 @@ mod tests { n } } - + { t { ...Frag1 @@ -2580,11 +2573,11 @@ mod tests { type Query { t: T } - + interface I { x: String } - + type T implements I { x: String a: String @@ -2598,7 +2591,7 @@ mod tests { a } } - + { t { ...FragI @@ -2622,12 +2615,12 @@ mod tests { type Query { t: T } - + type T { a: String u: U } - + type U { x: String y: String @@ -2638,7 +2631,7 @@ mod tests { fragment Frag1 on T { a } - + fragment Frag2 on T { u { x @@ -2646,13 +2639,13 @@ mod tests { } ...Frag1 } - + fragment Frag3 on Query { t { ...Frag2 } } - + { ...Frag3 } @@ -2677,16 +2670,16 @@ mod tests { type Query { t1: T1 } - + interface I { x: Int } - + type T1 implements I { x: Int y: Int } - + type T2 implements I { x: Int z: Int @@ -2702,7 +2695,7 @@ mod tests { z } } - + { t1 { ...FragOnI @@ -2725,24 +2718,24 @@ mod tests { type Query { i2: I2 } - + interface I1 { x: Int } - + interface I2 { y: Int } - + interface I3 { z: Int } - + type T1 implements I1 & I2 { x: Int y: Int } - + type T2 implements I1 & I3 { x: Int z: Int @@ -2758,7 +2751,7 @@ mod tests { z } } - + { i2 { ...FragOnI1 @@ -2788,13 +2781,13 @@ mod tests { type Query { t1: T1 } - + union U = T1 | T2 - + type T1 { x: Int } - + type T2 { y: Int } @@ -2809,7 +2802,7 @@ mod tests { y } } - + { t1 { ...OnU @@ -2919,18 +2912,18 @@ mod tests { type Query { t1: T1 } - + union U1 = T1 | T2 | T3 union U2 = T2 | T3 - + type T1 { x: Int } - + type T2 { y: Int } - + type T3 { z: Int } @@ -2942,7 +2935,7 @@ mod tests { ...Outer } } - + fragment Outer on U1 { ... on T1 { x @@ -2954,7 +2947,7 @@ mod tests { ... Inner } } - + fragment Inner on U2 { ... on T2 { y @@ -3013,23 +3006,23 @@ mod tests { type Query { t1: T1 } - + union U1 = T1 | T2 | T3 union U2 = T2 | T3 - + type T1 { x: Int } - + type T2 { y1: Y y2: Y } - + type T3 { z: Int } - + type Y { v: Int } @@ -3041,7 +3034,7 @@ mod tests { ...Outer } } - + fragment Outer on U1 { ... on T1 { x @@ -3053,7 +3046,7 @@ mod tests { ... Inner } } - + fragment Inner on U2 { ... on T2 { y1 { @@ -3064,7 +3057,7 @@ mod tests { } } } - + fragment WillBeUnused on Y { v } @@ -3099,14 +3092,14 @@ mod tests { t1: T t2: T } - + type T { a1: Int a2: Int b1: B b2: B } - + type B { x: Int y: Int @@ -3122,7 +3115,7 @@ mod tests { ...TFields } } - + fragment TFields on T { ...DirectFieldsOfT b1 { @@ -3132,12 +3125,12 @@ mod tests { ...BFields } } - + fragment DirectFieldsOfT on T { a1 a2 } - + fragment BFields on B { x y @@ -3220,7 +3213,7 @@ mod tests { t2: T t3: T } - + type T { a: Int b: Int @@ -3233,7 +3226,7 @@ mod tests { fragment DirectiveInDef on T { a @include(if: $cond1) } - + query myQuery($cond1: Boolean!, $cond2: Boolean!) { t1 { a @@ -3270,7 +3263,7 @@ mod tests { t2: T t3: T } - + type T { a: Int b: Int @@ -3283,7 +3276,7 @@ mod tests { fragment NoDirectiveDef on T { a } - + query myQuery($cond1: Boolean!) { t1 { ...NoDirectiveDef diff --git a/apollo-federation/src/operation/simplify.rs b/apollo-federation/src/operation/simplify.rs index 802bbfcab1..8555b241a2 100644 --- a/apollo-federation/src/operation/simplify.rs +++ b/apollo-federation/src/operation/simplify.rs @@ -460,7 +460,7 @@ impl SelectionSet { { match selection_or_set { SelectionOrSet::Selection(normalized_selection) => { - normalized_selections.add_local_selection(&normalized_selection, false)?; + normalized_selections.add_local_selection(&normalized_selection)?; } SelectionOrSet::SelectionSet(normalized_set) => { // Since the `selection` has been expanded/lifted, we use diff --git a/apollo-federation/src/operation/tests/defer.rs b/apollo-federation/src/operation/tests/defer.rs new file mode 100644 index 0000000000..8c37ea164d --- /dev/null +++ b/apollo-federation/src/operation/tests/defer.rs @@ -0,0 +1,194 @@ +use super::parse_operation; +use super::parse_schema; + +const DEFAULT_SCHEMA: &str = r#" +type A { + one: Int + two: Int + three: Int + b: B +} + +type B { + one: Boolean + two: Boolean + three: Boolean + a: A +} + +union AorB = A | B + +type Query { + a: A + b: B + either: AorB +} + +directive @defer(if: Boolean! = true, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT +"#; + +#[test] +fn without_defer_simple() { + let schema = parse_schema(DEFAULT_SCHEMA); + + let operation = parse_operation( + &schema, + r#" + { + ... @defer { a { one } } + b { + ... @defer { two } + } + } + "#, + ); + + let without_defer = operation.without_defer().unwrap(); + + insta::assert_snapshot!(without_defer, @r#" + { + ... { + a { + one + } + } + b { + ... { + two + } + } + } + "#); +} + +#[test] +fn without_defer_named_fragment() { + let schema = parse_schema(DEFAULT_SCHEMA); + + let operation = parse_operation( + &schema, + r#" + { + b { ...frag @defer } + either { ...frag } + } + fragment frag on B { + two + } + "#, + ); + + let without_defer = operation.without_defer().unwrap(); + + insta::assert_snapshot!(without_defer, @r#" + fragment frag on B { + two + } + + { + b { + ...frag + } + either { + ...frag + } + } + "#); +} + +#[test] +fn without_defer_merges_fragment() { + let schema = parse_schema(DEFAULT_SCHEMA); + + let operation = parse_operation( + &schema, + r#" + { + a { one } + either { + ... on B { + one + } + ... on B @defer { + two + } + } + } + "#, + ); + + let without_defer = operation.without_defer().unwrap(); + + insta::assert_snapshot!(without_defer, @r#" + { + a { + one + } + either { + ... on B { + one + two + } + } + } + "#); +} + +#[test] +fn without_defer_fragment_references() { + let schema = parse_schema(DEFAULT_SCHEMA); + + let operation = parse_operation( + &schema, + r#" + fragment a on A { + ... @defer { ...b } + } + fragment b on A { + one + b { + ...c @defer + } + } + fragment c on B { + two + } + fragment entry on Query { + a { ...a } + } + + { ...entry } + "#, + ); + + let without_defer = operation.without_defer().unwrap(); + + insta::assert_snapshot!(without_defer, @r###" + fragment c on B { + two + } + + fragment b on A { + one + b { + ...c + } + } + + fragment a on A { + ... { + ...b + } + } + + fragment entry on Query { + a { + ...a + } + } + + { + ...entry + } + "###); +} diff --git a/apollo-federation/src/operation/tests/mod.rs b/apollo-federation/src/operation/tests/mod.rs index 276b0a579f..b5e4d8e591 100644 --- a/apollo-federation/src/operation/tests/mod.rs +++ b/apollo-federation/src/operation/tests/mod.rs @@ -17,6 +17,8 @@ use crate::schema::position::ObjectTypeDefinitionPosition; use crate::schema::ValidFederationSchema; use crate::subgraph::Subgraph; +mod defer; + pub(super) fn parse_schema_and_operation( schema_and_operation: &str, ) -> (ValidFederationSchema, ExecutableDocument) { @@ -1204,7 +1206,7 @@ mod make_selection_tests { base_selection_set.type_position.clone(), selection.clone(), ); - Selection::from_element(base.element().unwrap(), Some(subselections), None).unwrap() + Selection::from_element(base.element().unwrap(), Some(subselections)).unwrap() }; let foo_with_a = clone_selection_at_path(foo, &[name!("a")]); @@ -1331,7 +1333,7 @@ mod lazy_map_tests { let field_element = Field::new_introspection_typename(s.schema(), &parent_type_pos, None); let typename_selection = - Selection::from_element(field_element.into(), /*subselection*/ None, None)?; + Selection::from_element(field_element.into(), /*subselection*/ None)?; // return `updated` and `typename_selection` Ok([updated, typename_selection].into_iter().collect()) }) diff --git a/apollo-federation/src/query_graph/build_query_graph.rs b/apollo-federation/src/query_graph/build_query_graph.rs index d4d2acf609..1bb2faa41d 100644 --- a/apollo-federation/src/query_graph/build_query_graph.rs +++ b/apollo-federation/src/query_graph/build_query_graph.rs @@ -1968,13 +1968,9 @@ impl FederatedQueryGraphBuilder { for edge in self.base.query_graph.graph.edge_indices() { let edge_weight = self.base.query_graph.edge_weight(edge)?; let (_, tail) = self.base.query_graph.edge_endpoints(edge)?; - let mut non_trivial_followups = IndexSet::default(); - for followup_edge_ref in self - .base - .query_graph - .graph - .edges_directed(tail, Direction::Outgoing) - { + let out_edges = self.base.query_graph.out_edges(tail); + let mut non_trivial_followups = Vec::with_capacity(out_edges.len()); + for followup_edge_ref in out_edges { let followup_edge_weight = followup_edge_ref.weight(); match edge_weight.transition { QueryGraphEdgeTransition::KeyResolution => { @@ -2035,7 +2031,7 @@ impl FederatedQueryGraphBuilder { } _ => {} } - non_trivial_followups.insert(followup_edge_ref.id()); + non_trivial_followups.push(followup_edge_ref.id()); } self.base .query_graph diff --git a/apollo-federation/src/query_graph/graph_path.rs b/apollo-federation/src/query_graph/graph_path.rs index 88bba9e8a0..af24a60d75 100644 --- a/apollo-federation/src/query_graph/graph_path.rs +++ b/apollo-federation/src/query_graph/graph_path.rs @@ -48,7 +48,6 @@ use crate::query_graph::QueryGraphEdgeTransition; use crate::query_graph::QueryGraphNodeType; use crate::query_plan::query_planner::EnabledOverrideConditions; use crate::query_plan::FetchDataPathElement; -use crate::query_plan::QueryPathElement; use crate::query_plan::QueryPlanCost; use crate::schema::position::AbstractTypeDefinitionPosition; use crate::schema::position::CompositeTypeDefinitionPosition; @@ -367,7 +366,7 @@ impl OpPathElement { ] { let directive_name: &'static str = (&kind).into(); if let Some(application) = self.directives().get(directive_name) { - let Some(arg) = application.argument_by_name("if") else { + let Some(arg) = application.specified_argument_by_name("if") else { return Err(FederationError::internal(format!( "@{} missing required argument \"if\"", directive_name @@ -426,12 +425,12 @@ impl OpPathElement { /// ignored). pub(crate) fn without_defer(&self) -> Option { match self { - Self::Field(_) => Some(self.clone()), // unchanged + Self::Field(_) => Some(self.clone()), Self::InlineFragment(inline_fragment) => { - // TODO(@goto-bus-stop): is this not exactly the wrong way around? let updated_directives: DirectiveList = inline_fragment .directives - .get_all("defer") + .iter() + .filter(|directive| directive.name != "defer") .cloned() .collect(); if inline_fragment.type_condition_position.is_none() @@ -501,16 +500,18 @@ impl OpGraphPathContext { &self, operation_element: &OpPathElement, ) -> Result { - let mut new_context = self.clone(); if operation_element.directives().is_empty() { - return Ok(new_context); + return Ok(self.clone()); } - let new_conditionals = operation_element.extract_operation_conditionals()?; - if !new_conditionals.is_empty() { - Arc::make_mut(&mut new_context.conditionals).extend(new_conditionals); + let mut new_conditionals = operation_element.extract_operation_conditionals()?; + if new_conditionals.is_empty() { + return Ok(self.clone()); } - Ok(new_context) + new_conditionals.extend(self.iter().cloned()); + Ok(OpGraphPathContext { + conditionals: Arc::new(new_conditionals), + }) } pub(crate) fn is_empty(&self) -> bool { @@ -3782,25 +3783,6 @@ impl OpPath { } } -impl TryFrom<&'_ OpPath> for Vec { - type Error = FederationError; - - fn try_from(value: &'_ OpPath) -> Result { - value - .0 - .iter() - .map(|path_element| { - Ok(match path_element.as_ref() { - OpPathElement::Field(field) => QueryPathElement::Field(field.try_into()?), - OpPathElement::InlineFragment(inline) => { - QueryPathElement::InlineFragment(inline.try_into()?) - } - }) - }) - .collect() - } -} - pub(crate) fn concat_paths_in_parents( first: &Option>, second: &Option>, @@ -3867,15 +3849,12 @@ fn is_useless_followup_element( }; let are_useless_directives = fragment.directives.is_empty() - || fragment - .directives - .iter() - .any(|d| !conditionals.contains(d)); + || fragment.directives.iter().all(|d| conditionals.contains(d)); let is_same_type = type_of_first.type_name() == type_of_second.type_name(); let is_subtype = first .schema() .schema() - .is_subtype(type_of_first.type_name(), type_of_second.type_name()); + .is_subtype(type_of_second.type_name(), type_of_first.type_name()); Ok(are_useless_directives && (is_same_type || is_subtype)) } }; diff --git a/apollo-federation/src/query_graph/mod.rs b/apollo-federation/src/query_graph/mod.rs index 4060ffdbe6..a69ba3cabf 100644 --- a/apollo-federation/src/query_graph/mod.rs +++ b/apollo-federation/src/query_graph/mod.rs @@ -355,7 +355,7 @@ pub struct QueryGraph { /// significantly faster (and pretty easy). FWIW, when originally introduced, this optimization /// lowered composition validation on a big composition (100+ subgraphs) from ~4 minutes to /// ~10 seconds. - non_trivial_followup_edges: IndexMap>, + non_trivial_followup_edges: IndexMap>, } impl QueryGraph { @@ -528,7 +528,7 @@ impl QueryGraph { }) } - pub(crate) fn non_trivial_followup_edges(&self) -> &IndexMap> { + pub(crate) fn non_trivial_followup_edges(&self) -> &IndexMap> { &self.non_trivial_followup_edges } @@ -906,7 +906,10 @@ impl QueryGraph { ty.directives() .get_all(&key_directive_definition.name) - .filter_map(|key| key.argument_by_name("fields").and_then(|arg| arg.as_str())) + .filter_map(|key| { + key.specified_argument_by_name("fields") + .and_then(|arg| arg.as_str()) + }) .map(|value| parse_field_set(schema, ty.name().clone(), value)) .find_ok(|selection| { !metadata diff --git a/apollo-federation/src/query_plan/conditions.rs b/apollo-federation/src/query_plan/conditions.rs index a91681de68..07388b7366 100644 --- a/apollo-federation/src/query_plan/conditions.rs +++ b/apollo-federation/src/query_plan/conditions.rs @@ -99,7 +99,7 @@ impl Conditions { "skip" => true, _ => continue, }; - let value = directive.argument_by_name("if").ok_or_else(|| { + let value = directive.specified_argument_by_name("if").ok_or_else(|| { FederationError::internal(format!( "missing if argument on @{}", if negated { "skip" } else { "include" }, @@ -223,12 +223,12 @@ pub(crate) fn remove_conditions_from_selection_set( selection.with_updated_selection_set(Some(updated_selection_set))? } } else { - Selection::from_element(updated_element, Some(updated_selection_set), None)? + Selection::from_element(updated_element, Some(updated_selection_set))? } } else if updated_element == element { selection.clone() } else { - Selection::from_element(updated_element, None, None)? + Selection::from_element(updated_element, None)? }; selection_map.insert(new_selection); } @@ -247,7 +247,7 @@ pub(crate) fn remove_conditions_from_selection_set( /// "starting" fragments having the unneeded condition/directives removed. pub(crate) fn remove_unneeded_top_level_fragment_directives( selection_set: &SelectionSet, - unneded_directives: &DirectiveList, + unneeded_directives: &DirectiveList, ) -> Result { let mut selection_map = SelectionMap::new(); @@ -265,7 +265,7 @@ pub(crate) fn remove_unneeded_top_level_fragment_directives( let needed_directives: Vec> = fragment .directives .iter() - .filter(|directive| !unneded_directives.contains(directive)) + .filter(|directive| !unneeded_directives.contains(directive)) .cloned() .collect(); @@ -273,7 +273,7 @@ pub(crate) fn remove_unneeded_top_level_fragment_directives( // at the "top-level" of the set. let updated_selections = remove_unneeded_top_level_fragment_directives( &inline_fragment.selection_set, - unneded_directives, + unneeded_directives, )?; if needed_directives.len() == fragment.directives.len() { // We need all the directives that the fragment has. Return it unchanged. @@ -346,7 +346,7 @@ fn matches_condition_for_kind( return false; } - let value = directive.argument_by_name("if"); + let value = directive.specified_argument_by_name("if"); let matches_if_negated = match kind { ConditionKind::Include => false, diff --git a/apollo-federation/src/query_plan/display.rs b/apollo-federation/src/query_plan/display.rs index a00efef669..b6416590e1 100644 --- a/apollo-federation/src/query_plan/display.rs +++ b/apollo-federation/src/query_plan/display.rs @@ -169,9 +169,9 @@ impl ConditionNode { state.indent()?; if_clause.write_indented(state)?; state.dedent()?; - state.write("},")?; + state.write("}")?; - state.write("Else {")?; + state.write(" Else {")?; state.indent()?; else_clause.write_indented(state)?; state.dedent()?; @@ -215,7 +215,7 @@ impl DeferNode { primary.write_indented(state)?; if !deferred.is_empty() { - state.write(", [")?; + state.write(" [")?; write_indented_lines(state, deferred, |state, deferred| { deferred.write_indented(state) })?; @@ -235,9 +235,12 @@ impl PrimaryDeferBlock { } = self; state.write("Primary {")?; if sub_selection.is_some() || node.is_some() { - state.indent()?; - if let Some(sub_selection) = sub_selection { + // Manually indent and write the newline + // to prevent a duplicate indent from `.new_line()` and `.initial_indent_level()`. + state.indent_no_new_line(); + state.write("\n")?; + state.write( sub_selection .serialize() @@ -247,7 +250,11 @@ impl PrimaryDeferBlock { state.write(":")?; state.new_line()?; } + } else { + // Indent to match the Some() case + state.indent()?; } + if let Some(node) = node { node.write_indented(state)?; } @@ -267,6 +274,7 @@ impl DeferredDeferBlock { sub_selection, node, } = self; + state.write("Deferred(depends: [")?; if let Some((DeferredDependency { id }, rest)) = depends.split_first() { state.write(id)?; @@ -285,16 +293,19 @@ impl DeferredDeferBlock { } state.write("\"")?; if let Some(label) = label { - state.write(", label: \"")?; - state.write(label)?; - state.write("\"")?; + state.write_fmt(format_args!(r#", label: "{label}""#))?; } state.write(") {")?; + if sub_selection.is_some() || node.is_some() { state.indent()?; if let Some(sub_selection) = sub_selection { write_selections(state, &sub_selection.selections)?; + state.write(":")?; + } + if sub_selection.is_some() && node.is_some() { + state.new_line()?; } if let Some(node) = node { node.write_indented(state)?; @@ -302,6 +313,7 @@ impl DeferredDeferBlock { state.dedent()?; } + state.write("},") } } diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index ad2134aaff..e3ab57f441 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -9,6 +9,7 @@ use apollo_compiler::ast::Argument; use apollo_compiler::ast::Directive; use apollo_compiler::ast::OperationType; use apollo_compiler::ast::Type; +use apollo_compiler::collections::HashMap; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::executable; @@ -27,6 +28,7 @@ use petgraph::visit::IntoNodeReferences; use serde::Serialize; use super::query_planner::SubgraphOperationCompression; +use crate::display_helpers::DisplayOption; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::link::graphql_definition::DeferDirectiveArguments; @@ -82,7 +84,65 @@ use crate::utils::logging::snapshot; type DeferRef = String; /// Map of defer labels to nodes of the fetch dependency graph. -type DeferredNodes = multimap::MultiMap>; +/// +/// Like a multimap with a Set instead of a Vec for value storage. +#[derive(Debug, Clone, Default)] +struct DeferredNodes { + inner: HashMap>>, +} +impl DeferredNodes { + fn new() -> Self { + Self::default() + } + + fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + fn insert(&mut self, defer_ref: DeferRef, node: NodeIndex) { + self.inner.entry(defer_ref).or_default().insert(node); + } + + fn get_all<'map>(&'map self, defer_ref: &DeferRef) -> Option<&'map IndexSet>> { + self.inner.get(defer_ref) + } + + fn iter(&self) -> impl Iterator)> { + self.inner + .iter() + .flat_map(|(defer_ref, nodes)| std::iter::repeat(defer_ref).zip(nodes.iter().copied())) + } + + /// Consume the map and yield each element. This is provided as a standalone method and not an + /// `IntoIterator` implementation because it's hard to type :) + fn into_iter(self) -> impl Iterator)> { + self.inner.into_iter().flat_map(|(defer_ref, nodes)| { + // Cloning the key is a bit wasteful, but keys are typically very small, + // and this map is also very small. + std::iter::repeat_with(move || defer_ref.clone()).zip(nodes) + }) + } +} +impl Extend<(DeferRef, NodeIndex)> for DeferredNodes { + fn extend)>>(&mut self, iter: T) { + for (defer_ref, node) in iter.into_iter() { + self.insert(defer_ref, node); + } + } +} +impl FromIterator<(DeferRef, NodeIndex)> for DeferredNodes { + fn from_iter)>>(iter: T) -> Self { + let mut nodes = Self::new(); + nodes.extend(iter); + nodes + } +} +impl FromIterator<(DeferRef, IndexSet>)> for DeferredNodes { + fn from_iter>)>>(iter: T) -> Self { + let inner = iter.into_iter().collect(); + Self { inner } + } +} /// Represents a subgraph fetch of a query plan. // PORT_NOTE: The JS codebase called this `FetchGroup`, but this naming didn't make it apparent that @@ -131,14 +191,14 @@ pub(crate) struct FetchDependencyGraphNode { /// Safely generate IDs for fetch dependency nodes without mutable access. #[derive(Debug)] -struct FetchIdGenerator { +pub(crate) struct FetchIdGenerator { next: AtomicU64, } impl FetchIdGenerator { /// Create an ID generator, starting at the given value. - pub fn new(start_at: u64) -> Self { + pub(crate) fn new() -> Self { Self { - next: AtomicU64::new(start_at), + next: AtomicU64::new(0), } } @@ -148,14 +208,6 @@ impl FetchIdGenerator { } } -impl Clone for FetchIdGenerator { - fn clone(&self) -> Self { - Self { - next: AtomicU64::new(self.next.load(std::sync::atomic::Ordering::Relaxed)), - } - } -} - #[derive(Debug, Clone, Serialize)] pub(crate) struct FetchSelectionSet { /// The selection set to be fetched from the subgraph. @@ -220,11 +272,9 @@ pub(crate) struct FetchDependencyGraph { // serialized output will be needed. #[serde(skip)] pub(crate) defer_tracking: DeferTracking, - /// The initial fetch ID generation (used when handling `@defer`). - starting_id_generation: u64, /// The current fetch ID generation (used when handling `@defer`). #[serde(skip)] - fetch_id_generation: FetchIdGenerator, + pub(crate) fetch_id_generation: Arc, /// Whether this fetch dependency graph has undergone a transitive reduction. is_reduced: bool, /// Whether this fetch dependency graph has undergone optimization (e.g. transitive reduction, @@ -647,7 +697,7 @@ impl FetchDependencyGraph { supergraph_schema: ValidFederationSchema, federated_query_graph: Arc, root_type_for_defer: Option, - starting_id_generation: u64, + fetch_id_generation: Arc, ) -> Self { Self { defer_tracking: DeferTracking::empty(&supergraph_schema, root_type_for_defer), @@ -655,8 +705,7 @@ impl FetchDependencyGraph { federated_query_graph, graph: Default::default(), root_nodes_by_subgraph: Default::default(), - starting_id_generation, - fetch_id_generation: FetchIdGenerator::new(starting_id_generation), + fetch_id_generation, is_reduced: false, is_optimized: false, } @@ -1691,11 +1740,12 @@ impl FetchDependencyGraph { if node.defer_ref == child.defer_ref { children.push(child_index); } else { - let parent_defer_ref = node.defer_ref.as_ref().unwrap(); let Some(child_defer_ref) = &child.defer_ref else { - panic!("{} has defer_ref `{parent_defer_ref}`, so its child {} cannot have a top-level defer_ref.", - node.display(node_index), - child.display(child_index), + panic!( + "{} has defer_ref `{}`, so its child {} cannot have a top-level defer_ref.", + node.display(node_index), + DisplayOption(node.defer_ref.as_ref()), + child.display(child_index), ); }; @@ -1814,7 +1864,7 @@ impl FetchDependencyGraph { let (main, deferred_nodes, state_after_node) = self.process_node(processor, *node_index, handled_conditions.clone())?; processed_nodes.push(main); - all_deferred_nodes.extend(deferred_nodes); + all_deferred_nodes.extend(deferred_nodes.into_iter()); new_state = new_state.merge_with(state_after_node); } @@ -1859,7 +1909,7 @@ impl FetchDependencyGraph { process_in_parallel = true; main_sequence.push(processed); state = new_state; - all_deferred_nodes.extend(deferred_nodes); + all_deferred_nodes.extend(deferred_nodes.into_iter()); } Ok((main_sequence, all_deferred_nodes, state)) @@ -1897,7 +1947,7 @@ impl FetchDependencyGraph { .join(", "), ); let mut all_deferred_nodes = other_defer_nodes.cloned().unwrap_or_default(); - all_deferred_nodes.extend(deferred_nodes); + all_deferred_nodes.extend(deferred_nodes.into_iter()); // We're going to handle all `@defer`s at our "current" level (eg. at the top level, that's all the non-nested @defer), // and the "starting" node for those defers, if any, are in `all_deferred_nodes`. However, `all_deferred_nodes` @@ -1913,14 +1963,9 @@ impl FetchDependencyGraph { .map(|info| info.label.clone()) .collect::>(); let unhandled_defer_nodes = all_deferred_nodes - .keys() - .filter(|label| !handled_defers_in_current.contains(*label)) - .map(|label| { - ( - label.clone(), - all_deferred_nodes.get_vec(label).cloned().unwrap(), - ) - }) + .iter() + .filter(|(label, _index)| !handled_defers_in_current.contains(*label)) + .map(|(label, index)| (label.clone(), index)) .collect::(); let unhandled_defer_node = if unhandled_defer_nodes.is_empty() { None @@ -1938,9 +1983,10 @@ impl FetchDependencyGraph { let defers_in_current = defers_in_current.into_iter().cloned().collect::>(); for defer in defers_in_current { let nodes = all_deferred_nodes - .get_vec(&defer.label) - .cloned() - .unwrap_or_default(); + .get_all(&defer.label) + .map_or_else(Default::default, |indices| { + indices.iter().copied().collect() + }); let (main_sequence_of_defer, deferred_of_defer) = self.process_root_nodes( processor, nodes, @@ -2048,29 +2094,34 @@ impl FetchDependencyGraph { fn can_merge_grand_child_in( &self, node_id: NodeIndex, - grand_child_id: NodeIndex, + child_id: NodeIndex, + maybe_grand_child_id: NodeIndex, ) -> Result { - let grand_child_parent_relations: Vec = - self.parents_relations_of(grand_child_id).collect(); - if grand_child_parent_relations.len() != 1 { + let Some(grand_child_parent_relation) = + iter_into_single_item(self.parents_relations_of(maybe_grand_child_id)) + else { return Ok(false); - } + }; + let Some(grand_child_parent_parent_relation) = + self.parent_relation(grand_child_parent_relation.parent_node_id, node_id) + else { + return Ok(false); + }; let node = self.node_weight(node_id)?; - let grand_child = self.node_weight(grand_child_id)?; - let grand_child_parent_parent_relation = - self.parent_relation(grand_child_parent_relations[0].parent_node_id, node_id); + let child = self.node_weight(child_id)?; + let grand_child = self.node_weight(maybe_grand_child_id)?; - let (Some(node_inputs), Some(grand_child_inputs)) = (&node.inputs, &grand_child.inputs) + let (Some(child_inputs), Some(grand_child_inputs)) = (&child.inputs, &grand_child.inputs) else { return Ok(false); }; // we compare the subgraph names last because on average it improves performance - Ok(grand_child_parent_relations[0].path_in_parent.is_some() - && grand_child_parent_parent_relation.is_some_and(|r| r.path_in_parent.is_some()) - && node.merge_at == grand_child.merge_at - && node_inputs.contains(grand_child_inputs) + Ok(grand_child_parent_relation.path_in_parent.is_some() + && grand_child_parent_parent_relation.path_in_parent.is_some() + && child.merge_at == grand_child.merge_at + && child_inputs.contains(grand_child_inputs) && node.defer_ref == grand_child.defer_ref && node.subgraph_name == grand_child.subgraph_name) } @@ -2938,7 +2989,6 @@ fn operation_for_entities_fetch( sibling_typename: None, })), Some(selection_set), - None, )?; let type_position: CompositeTypeDefinitionPosition = subgraph_schema @@ -3050,8 +3100,7 @@ impl FetchSelectionSet { path_in_node: &OpPath, selection_set: Option<&Arc>, ) -> Result<(), FederationError> { - let target = Arc::make_mut(&mut self.selection_set); - target.add_at_path(path_in_node, selection_set)?; + Arc::make_mut(&mut self.selection_set).add_at_path(path_in_node, selection_set)?; // TODO: when calling this multiple times, maybe only re-compute conditions at the end? // Or make it lazily-initialized and computed on demand? self.conditions = self.selection_set.conditions()?; @@ -3211,7 +3260,8 @@ impl DeferTracking { .label .as_ref() .expect("All @defer should have been labeled at this point"); - let _deferred_block = self.deferred.entry(label.clone()).or_insert_with(|| { + + self.deferred.entry(label.clone()).or_insert_with(|| { DeferredInfo::empty( primary_selection.schema.clone(), label.clone(), @@ -3446,7 +3496,7 @@ fn compute_nodes_for_key_resolution<'a>( conditions, stack_item.node_id, stack_item.node_path.clone(), - stack_item.defer_context.clone(), + stack_item.defer_context.for_conditions(), &Default::default(), )?; created_nodes.extend(conditions_nodes.iter().copied()); @@ -3677,12 +3727,12 @@ fn compute_nodes_for_root_type_resolution<'a>( }) } -#[cfg_attr(feature = "snapshot_tracing", tracing::instrument(skip_all, level = "trace", fields(label = operation.to_string())))] +#[cfg_attr(feature = "snapshot_tracing", tracing::instrument(skip_all, level = "trace", fields(label = operation_element.to_string())))] fn compute_nodes_for_op_path_element<'a>( dependency_graph: &mut FetchDependencyGraph, stack_item: &ComputeNodesStackItem<'a>, child: &'a Arc>>, - operation: &OpPathElement, + operation_element: &OpPathElement, created_nodes: &mut IndexSet, ) -> Result, FederationError> { let Some(edge_id) = child.edge else { @@ -3693,7 +3743,7 @@ fn compute_nodes_for_op_path_element<'a>( // to one for the defer in question. let (updated_operation, updated_defer_context) = extract_defer_from_operation( dependency_graph, - operation, + operation_element, &stack_item.defer_context, &stack_item.node_path, )?; @@ -3720,19 +3770,19 @@ fn compute_nodes_for_op_path_element<'a>( let dest = stack_item.tree.graph.node_weight(dest_id)?; if source.source != dest.source { return Err(FederationError::internal(format!( - "Collecting edge {edge_id:?} for {operation:?} \ - should not change the underlying subgraph" + "Collecting edge {edge_id:?} for {operation_element:?} \ + should not change the underlying subgraph" ))); } // We have a operation element, field or inline fragment. // We first check if it's been "tagged" to remember that __typename must be queried. // See the comment on the `optimize_sibling_typenames()` method to see why this exists. - if let Some(sibling_typename) = operation.sibling_typename() { + if let Some(sibling_typename) = operation_element.sibling_typename() { // We need to add the query __typename for the current type in the current node. let typename_field = Arc::new(OpPathElement::Field(Field::new_introspection_typename( - operation.schema(), - &operation.parent_type_position(), + operation_element.schema(), + &operation_element.parent_type_position(), sibling_typename.alias().cloned(), ))); let typename_path = stack_item @@ -3757,12 +3807,12 @@ fn compute_nodes_for_op_path_element<'a>( } let Ok((Some(updated_operation), updated_defer_context)) = extract_defer_from_operation( dependency_graph, - operation, + operation_element, &stack_item.defer_context, &stack_item.node_path, ) else { return Err(FederationError::internal(format!( - "Extracting @defer from {operation:?} should not have resulted in no operation" + "Extracting @defer from {operation_element:?} should not have resulted in no operation" ))); }; let mut updated = ComputeNodesStackItem { @@ -3995,15 +4045,15 @@ fn compute_input_rewrites_on_key_fetch( /// - The updated operation can be `None`, if operation is no longer necessary. fn extract_defer_from_operation( dependency_graph: &mut FetchDependencyGraph, - operation: &OpPathElement, + operation_element: &OpPathElement, defer_context: &DeferContext, node_path: &FetchDependencyGraphNodePath, ) -> Result<(Option, DeferContext), FederationError> { - let defer_args = operation.defer_directive_args(); + let defer_args = operation_element.defer_directive_args(); let Some(defer_args) = defer_args else { let updated_path_to_defer_parent = defer_context .path_to_defer_parent - .with_pushed(operation.clone().into()); + .with_pushed(operation_element.clone().into()); let updated_context = DeferContext { path_to_defer_parent: updated_path_to_defer_parent.into(), // Following fields are identical to those of `defer_context`. @@ -4011,16 +4061,16 @@ fn extract_defer_from_operation( active_defer_ref: defer_context.active_defer_ref.clone(), is_part_of_query: defer_context.is_part_of_query, }; - return Ok((Some(operation.clone()), updated_context)); + return Ok((Some(operation_element.clone()), updated_context)); }; - let updated_defer_ref = defer_args.label.as_ref().ok_or_else(|| - // PORT_NOTE: The original TypeScript code has an assertion here. - FederationError::internal( - "All defers should have a label at this point", - ))?; - let updated_operation = operation.without_defer(); - let updated_path_to_defer_parent = match updated_operation { + // PORT_NOTE: The original TypeScript code has an assertion here. + let updated_defer_ref = defer_args + .label + .as_ref() + .ok_or_else(|| FederationError::internal("All defers should have a label at this point"))?; + let updated_operation_element = operation_element.without_defer(); + let updated_path_to_defer_parent = match updated_operation_element { None => Default::default(), // empty OpPath Some(ref updated_operation) => OpPath(vec![Arc::new(updated_operation.clone())]), }; @@ -4029,7 +4079,7 @@ fn extract_defer_from_operation( defer_context, &defer_args, node_path.clone(), - operation.parent_type_position(), + operation_element.parent_type_position(), )?; let updated_context = DeferContext { @@ -4039,7 +4089,7 @@ fn extract_defer_from_operation( active_defer_ref: defer_context.active_defer_ref.clone(), is_part_of_query: defer_context.is_part_of_query, }; - Ok((updated_operation, updated_context)) + Ok((updated_operation_element, updated_context)) } fn handle_requires( @@ -4114,7 +4164,7 @@ fn handle_requires( requires_conditions, new_node_id, fetch_node_path.clone(), - defer_context_for_conditions(defer_context), + defer_context.for_conditions(), &OpGraphPathContext::default(), )?; if newly_created_node_ids.is_empty() { @@ -4233,9 +4283,11 @@ fn handle_requires( // can merge the node). if parent.path_in_parent.is_some() { for created_node_id in newly_created_node_ids { - if dependency_graph - .can_merge_grand_child_in(parent.parent_node_id, created_node_id)? - { + if dependency_graph.can_merge_grand_child_in( + parent.parent_node_id, + fetch_node_id, + created_node_id, + )? { dependency_graph .merge_grand_child_in(parent.parent_node_id, created_node_id)?; } else { @@ -4346,7 +4398,7 @@ fn handle_requires( requires_conditions, fetch_node_id, fetch_node_path.clone(), - defer_context_for_conditions(defer_context), + defer_context.for_conditions(), &OpGraphPathContext::default(), )?; // If we didn't create any node, that means the whole condition was fetched from the current node @@ -4415,11 +4467,14 @@ fn handle_requires( } } -fn defer_context_for_conditions(base_context: &DeferContext) -> DeferContext { - let mut context = base_context.clone(); - context.is_part_of_query = false; - context.current_defer_ref = base_context.active_defer_ref.clone(); - context +impl DeferContext { + /// Create a sub-context for use in resolving conditions inside an @defer block. + fn for_conditions(&self) -> Self { + let mut context = self.clone(); + context.is_part_of_query = false; + context.current_defer_ref = self.active_defer_ref.clone(); + context + } } fn inputs_for_require( diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph_processor.rs b/apollo-federation/src/query_plan/fetch_dependency_graph_processor.rs index ab126dbcc6..37982b3ee2 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph_processor.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph_processor.rs @@ -6,9 +6,11 @@ use apollo_compiler::Name; use apollo_compiler::Node; use super::query_planner::SubgraphOperationCompression; +use super::QueryPathElement; use crate::error::FederationError; use crate::operation::DirectiveList; use crate::operation::SelectionSet; +use crate::query_graph::graph_path::OpPathElement; use crate::query_graph::QueryGraph; use crate::query_plan::conditions::Conditions; use crate::query_plan::fetch_dependency_graph::DeferredInfo; @@ -338,6 +340,32 @@ impl FetchDependencyGraphProcessor, DeferredDeferBlock> defer_info: &DeferredInfo, node: Option, ) -> Result { + /// Produce a query path with only the relevant elements: fields and type conditions. + fn op_path_to_query_path( + path: &[Arc], + ) -> Result, FederationError> { + path.iter() + .map( + |element| -> Result, FederationError> { + match &**element { + OpPathElement::Field(field) => { + Ok(Some(QueryPathElement::Field(field.try_into()?))) + } + OpPathElement::InlineFragment(inline) => { + match &inline.type_condition_position { + Some(_) => Ok(Some(QueryPathElement::InlineFragment( + inline.try_into()?, + ))), + None => Ok(None), + } + } + } + }, + ) + .filter_map(|result| result.transpose()) + .collect::, _>>() + } + Ok(DeferredDeferBlock { depends: defer_info .dependencies @@ -354,7 +382,7 @@ impl FetchDependencyGraphProcessor, DeferredDeferBlock> } else { Some(defer_info.label.clone()) }, - query_path: defer_info.path.full_path.as_ref().try_into()?, + query_path: op_path_to_query_path(&defer_info.path.full_path)?, // Note that if the deferred block has nested @defer, // then the `value` is going to be a `DeferNode` // and we'll use it's own `subselection`, so we don't need it here. diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index a64f79364c..6d75453422 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -12,11 +12,14 @@ use apollo_compiler::Name; use itertools::Itertools; use serde::Serialize; +use super::fetch_dependency_graph::FetchIdGenerator; +use super::ConditionNode; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::link::federation_spec_definition::FederationSpecDefinition; use crate::operation::normalize_operation; use crate::operation::NamedFragments; +use crate::operation::NormalizedDefer; use crate::operation::Operation; use crate::operation::SelectionSet; use crate::query_graph::build_federated_query_graph; @@ -407,37 +410,29 @@ impl QueryPlanner { &self.interface_types_with_interface_objects, )?; - let (normalized_operation, assigned_defer_labels, defer_conditions, has_defers) = ( - normalized_operation.without_defer(), - None, - None::>>, - false, - ); - /* TODO(TylerBloom): After defer is impl-ed and after the private preview, the call - * above needs to be replaced with this if-else expression. - if self.config.incremental_delivery.enable_defer { - let NormalizedDefer { - operation, - assigned_defer_labels, - defer_conditions, - has_defers, - } = normalized_operation.with_normalized_defer(); - if has_defers && is_subscription { - return Err(SingleFederationError::DeferredSubscriptionUnsupported.into()); - } - ( - operation, - Some(assigned_defer_labels), - Some(defer_conditions), - has_defers, - ) - } else { - // If defer is not enabled, we remove all @defer from the query. This feels cleaner do this once here than - // having to guard all the code dealing with defer later, and is probably less error prone too (less likely - // to end up passing through a @defer to a subgraph by mistake). - (normalized_operation.without_defer(), None, None, false) - }; - */ + let (normalized_operation, assigned_defer_labels, defer_conditions, has_defers) = + if self.config.incremental_delivery.enable_defer { + let NormalizedDefer { + operation, + assigned_defer_labels, + defer_conditions, + has_defers, + } = normalized_operation.with_normalized_defer()?; + if has_defers && is_subscription { + return Err(SingleFederationError::DeferredSubscriptionUnsupported.into()); + } + ( + operation, + Some(assigned_defer_labels), + Some(defer_conditions), + has_defers, + ) + } else { + // If defer is not enabled, we remove all @defer from the query. This feels cleaner do this once here than + // having to guard all the code dealing with defer later, and is probably less error prone too (less likely + // to end up passing through a @defer to a subgraph by mistake). + (normalized_operation.without_defer()?, None, None, false) + }; if normalized_operation.selection_set.is_empty() { return Ok(QueryPlan::default()); @@ -503,11 +498,16 @@ impl QueryPlanner { override_conditions: EnabledOverrideConditions(HashSet::from_iter( options.override_conditions, )), + fetch_id_generator: Arc::new(FetchIdGenerator::new()), }; let root_node = match defer_conditions { Some(defer_conditions) if !defer_conditions.is_empty() => { - compute_plan_for_defer_conditionals(&mut parameters, defer_conditions)? + compute_plan_for_defer_conditionals( + &mut parameters, + &mut processor, + defer_conditions, + )? } _ => compute_plan_internal(&mut parameters, &mut processor, has_defers)?, }; @@ -621,7 +621,6 @@ fn compute_root_serial_dependency_graph( // We have to serially compute a plan for each top-level selection. let mut split_roots = operation.selection_set.clone().split_top_level_fields(); let mut digest = Vec::new(); - let mut starting_fetch_id = 0; let selection_set = split_roots .next() .ok_or_else(|| FederationError::internal("Empty top level fields"))?; @@ -653,7 +652,7 @@ fn compute_root_serial_dependency_graph( supergraph_schema.clone(), federated_query_graph.clone(), root_type.clone(), - starting_fetch_id, + fetch_dependency_graph.fetch_id_generation.clone(), ); compute_root_fetch_groups( operation.root_kind, @@ -666,7 +665,6 @@ fn compute_root_serial_dependency_graph( // the current ID that is inside the fetch dep graph's ID generator, or to use the // starting ID. Because this method ensure uniqueness between IDs, this approach was // taken; however, it could be the case that this causes unforseen issues. - starting_fetch_id = fetch_dependency_graph.next_fetch_id(); digest.push(std::mem::replace( &mut fetch_dependency_graph, new_dep_graph, @@ -835,16 +833,45 @@ fn compute_plan_internal( } } -// TODO: FED-95 fn compute_plan_for_defer_conditionals( - _parameters: &mut QueryPlanningParameters, - _defer_conditions: IndexMap>, + parameters: &mut QueryPlanningParameters, + processor: &mut FetchDependencyGraphToQueryPlanProcessor, + defer_conditions: IndexMap>, ) -> Result, FederationError> { - Err(SingleFederationError::UnsupportedFeature { - message: String::from("@defer is currently not supported"), - kind: crate::error::UnsupportedFeatureKind::Defer, + generate_condition_nodes( + parameters.operation.clone(), + defer_conditions.iter(), + &mut |op| { + parameters.operation = op; + compute_plan_internal(parameters, processor, true) + }, + ) +} + +fn generate_condition_nodes<'a>( + op: Arc, + mut conditions: impl Clone + Iterator)>, + on_final_operation: &mut impl FnMut(Arc) -> Result, FederationError>, +) -> Result, FederationError> { + match conditions.next() { + None => on_final_operation(op), + Some((cond, labels)) => { + let else_op = Arc::unwrap_or_clone(op.clone()).reduce_defer(labels)?; + let if_op = op; + let node = ConditionNode { + condition_variable: cond.clone(), + if_clause: generate_condition_nodes(if_op, conditions.clone(), on_final_operation)? + .map(Box::new), + else_clause: generate_condition_nodes( + Arc::new(else_op), + conditions.clone(), + on_final_operation, + )? + .map(Box::new), + }; + Ok(Some(PlanNode::Condition(Box::new(node)))) + } } - .into()) } /// Tracks fragments from the original operation, along with versions rebased on other subgraphs. diff --git a/apollo-federation/src/query_plan/query_planning_traversal.rs b/apollo-federation/src/query_plan/query_planning_traversal.rs index cb5e6bb9f1..0fba2e801a 100644 --- a/apollo-federation/src/query_plan/query_planning_traversal.rs +++ b/apollo-federation/src/query_plan/query_planning_traversal.rs @@ -6,6 +6,7 @@ use petgraph::graph::NodeIndex; use serde::Serialize; use tracing::trace; +use super::fetch_dependency_graph::FetchIdGenerator; use crate::error::FederationError; use crate::operation::Operation; use crate::operation::Selection; @@ -60,6 +61,7 @@ pub(crate) struct QueryPlanningParameters<'a> { pub(crate) federated_query_graph: Arc, /// The operation to be query planned. pub(crate) operation: Arc, + pub(crate) fetch_id_generator: Arc, /// The query graph node at which query planning begins. pub(crate) head: NodeIndex, /// Whether the head must be a root node for query planning. @@ -84,8 +86,9 @@ pub(crate) struct QueryPlanningTraversal<'a, 'b> { /// True if query planner `@defer` support is enabled and the operation contains some `@defer` /// application. has_defers: bool, - /// The initial fetch ID generation (used when handling `@defer`). - starting_id_generation: u64, + /// A handle to the sole generator of fetch IDs. While planning an operation, only one of + /// generator can be used. + id_generator: Arc, /// A processor for converting fetch dependency graphs to cost. cost_processor: FetchDependencyGraphToCostProcessor, /// True if this query planning is at top-level (note that query planning can recursively start @@ -146,7 +149,7 @@ impl BestQueryPlanInfo { parameters.supergraph_schema.clone(), parameters.federated_query_graph.clone(), None, - 0, + parameters.fetch_id_generator.clone(), ), path_tree: OpPathTree::new(parameters.federated_query_graph.clone(), parameters.head) .into(), @@ -174,8 +177,8 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { Self::new_inner( parameters, selection_set, - 0, has_defers, + parameters.fetch_id_generator.clone(), root_kind, cost_processor, Default::default(), @@ -193,8 +196,8 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { fn new_inner( parameters: &'a QueryPlanningParameters, selection_set: SelectionSet, - starting_id_generation: u64, has_defers: bool, + id_generator: Arc, root_kind: SchemaRootDefinitionKind, cost_processor: FetchDependencyGraphToCostProcessor, initial_context: OpGraphPathContext, @@ -233,7 +236,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { parameters, root_kind, has_defers, - starting_id_generation, + id_generator, cost_processor, is_top_level, open_branches: Default::default(), @@ -615,7 +618,6 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { if self.closed_branches.is_empty() { return Ok(()); } - self.prune_closed_branches(); self.sort_options_in_closed_branches()?; self.reduce_options_if_needed(); @@ -732,50 +734,6 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { Ok(()) } - /// Remove closed branches that are known to be overridden by others. - /// - /// We've computed all branches and need to compare all the possible plans to pick the best. - /// Note however that "all the possible plans" is essentially a cartesian product of all - /// the closed branches options, and if a lot of branches have multiple options, this can - /// exponentially explode. - /// So first, we check if we can preemptively prune some branches based on - /// those branches having options that are known to be overriden by other ones. - fn prune_closed_branches(&mut self) { - for branch in &mut self.closed_branches { - if branch.0.len() <= 1 { - continue; - } - - let mut pruned = ClosedBranch(Vec::new()); - for (i, to_check) in branch.0.iter().enumerate() { - if !Self::option_is_overriden(i, &to_check.paths, branch) { - pruned.0.push(to_check.clone()); - } - } - - *branch = pruned - } - } - - fn option_is_overriden( - index: usize, - to_check: &SimultaneousPaths, - all_options: &ClosedBranch, - ) -> bool { - all_options - .0 - .iter() - .enumerate() - // Don’t compare `to_check` with itself - .filter(|&(i, _)| i != index) - .any(|(_i, option)| { - to_check - .0 - .iter() - .all(|p| option.paths.0.iter().any(|o| p.is_overridden_by(o))) - }) - } - /// We now sort the options within each branch, /// putting those with the least amount of subgraph jumps first. /// The idea is that for each branch taken individually, @@ -958,7 +916,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { self.parameters.supergraph_schema.clone(), self.parameters.federated_query_graph.clone(), root_type, - self.starting_id_generation, + self.id_generator.clone(), ) } @@ -1061,12 +1019,13 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { config: self.parameters.config.clone(), statistics: self.parameters.statistics, override_conditions: self.parameters.override_conditions.clone(), + fetch_id_generator: self.parameters.fetch_id_generator.clone(), }; let best_plan_opt = QueryPlanningTraversal::new_inner( ¶meters, edge_conditions.clone(), - self.starting_id_generation, self.has_defers, + self.id_generator.clone(), self.root_kind, self.cost_processor, context.clone(), diff --git a/apollo-federation/src/supergraph/mod.rs b/apollo-federation/src/supergraph/mod.rs index 01210155d9..bc8893f3d5 100644 --- a/apollo-federation/src/supergraph/mod.rs +++ b/apollo-federation/src/supergraph/mod.rs @@ -2242,7 +2242,7 @@ fn extract_join_directives( fn join_directive_to_real_directive(directive: &Node) -> (Directive, Vec) { let subgraph_enum_values = directive - .argument_by_name("graphs") + .specified_argument_by_name("graphs") .and_then(|arg| arg.as_list()) .map(|list| { list.iter() @@ -2259,13 +2259,13 @@ fn join_directive_to_real_directive(directive: &Node) -> (Directive, .expect("join__directive(graphs:) missing"); let name = directive - .argument_by_name("name") + .specified_argument_by_name("name") .expect("join__directive(name:) is present") .as_str() .expect("join__directive(name:) is a string"); let arguments = directive - .argument_by_name("args") + .specified_argument_by_name("args") .and_then(|a| a.as_object()) .map(|args| { args.iter() diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests.rs b/apollo-federation/tests/query_plan/build_query_plan_tests.rs index acef092c2c..1f9fd8587c 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests.rs @@ -32,6 +32,7 @@ fn some_name() { */ mod debug_max_evaluated_plans_configuration; +mod defer; mod fetch_operation_names; mod field_merging_with_skip_and_include; mod fragment_autogeneration; @@ -1259,8 +1260,8 @@ fn handles_multiple_conditions_on_abstract_types() { } } => { - ... on Book @skip(if: $title) { - ... on Book @include(if: $title) { + ... on Book @include(if: $title) { + ... on Book @skip(if: $title) { sku } } diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/defer.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/defer.rs new file mode 100644 index 0000000000..2dd9f28cdd --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/defer.rs @@ -0,0 +1,3826 @@ +use apollo_federation::query_plan::query_planner::QueryPlannerConfig; + +fn config_with_defer() -> QueryPlannerConfig { + let mut config = QueryPlannerConfig::default(); + config.incremental_delivery.enable_defer = true; + config +} + +#[test] +fn defer_test_handles_simple_defer_without_defer_enabled() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v1: Int + v2: Int + } + "#, + ); + // without defer-support enabled + assert_plan!(planner, + r#" + { + t { + v1 + ... @defer { + v2 + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + v2 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn defer_test_normalizes_if_false() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v1: Int + v2: Int + } + "#, + ); + assert_plan!(planner, + r#" + { + t { + v1 + ... @defer(if: false) { + v2 + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + v2 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn defer_test_normalizes_if_true() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v1: Int + v2: Int + } + "#, + ); + assert_plan!(planner, + r#" + { + t { + v1 + ... @defer(if: true) { + v2 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v1 + } + }: + Sequence { + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + }, + }, [ + Deferred(depends: [0], path: "t") { + { + v2 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_handles_simple_defer_with_defer_enabled() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v1: Int + v2: Int + } + "#, + ); + assert_plan!(planner, + r#" + { + t { + v1 + ... @defer { + v2 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v1 + } + }: + Sequence { + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + }, + }, [ + Deferred(depends: [0], path: "t") { + { + v2 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_non_router_based_defer_case_one() { + // @defer on value type + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v: V + } + + type V { + a: Int + b: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v { + a + ... @defer { + b + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v { + a + } + } + }: + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v { + a + b + } + } + } + }, + }, + }, + }, [ + Deferred(depends: [], path: "t/v") { + { + b + }: + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_non_router_based_defer_case_two() { + // @defer on entity but with no @key + // While the @defer in the operation is on an entity, the @key in the first subgraph + // is explicitely marked as non-resovable, so we cannot use it to actually defer the + // fetch to `v1`. Note that example still compose because, defer excluded, `v1` can + // still be fetched for all queries (which is only `t` here). + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id", resolvable: false) { + id: ID! + v1: String + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v2: String + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + ... @defer { + v1 + } + v2 + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v2 + } + }: + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + v1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + }, [ + Deferred(depends: [], path: "t") { + { + v1 + }: + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_non_router_based_defer_case_three() { + // @defer on value type but with entity afterwards + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + + type U @key(fields: "id") { + id: ID! + x: Int + } + "#, + + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v: V + } + + type V { + a: Int + u: U + } + + type U @key(fields: "id") { + id: ID! + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v { + a + ... @defer { + u { + x + } + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v { + a + } + } + }: + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2", id: 0) { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v { + a + u { + __typename + id + } + } + } + } + }, + }, + }, + }, [ + Deferred(depends: [0], path: "t/v") { + { + u { + x + } + }: + Flatten(path: "t.v.u") { + Fetch(service: "Subgraph1") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + x + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_resuming_in_the_same_subgraph() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + v0: String + v1: String + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v0 + ... @defer { + v1 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v0 + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + v0 + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + v1 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_multiple_fields_in_different_subgraphs() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + v0: String + v1: String + } + "#, + + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v2: String + } + "#, + Subgraph3: r#" + type T @key(fields: "id") { + id: ID! + v3: String + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v0 + ... @defer { + v1 + v2 + v3 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v0 + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + v0 + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + v1 + v2 + v3 + }: + Parallel { + Flatten(path: "t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph3") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v3 + } + } + }, + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_multiple_non_nested_defer_plus_label_handling() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + v0: String + v1: String + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v2: String + v3: U + } + + type U @key(fields: "id") { + id: ID! + } + "#, + Subgraph3: r#" + type U @key(fields: "id") { + id: ID! + x: Int + y: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v0 + ... @defer(label: "defer_v1") { + v1 + } + ... @defer { + v2 + } + v3 { + x + ... @defer(label: "defer_in_v3") { + y + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v0 + v3 { + x + } + } + }: + Sequence { + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + id + v0 + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2", id: 1) { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v3 { + __typename + id + } + } + } + }, + }, + Flatten(path: "t.v3") { + Fetch(service: "Subgraph3") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + x + } + } + }, + }, + }, + }, [ + Deferred(depends: [0], path: "t") { + { + v2 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + Deferred(depends: [0], path: "t", label: "defer_v1") { + { + v1 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + }, + Deferred(depends: [1], path: "t/v3", label: "defer_in_v3") { + { + y + }: + Flatten(path: "t.v3") { + Fetch(service: "Subgraph3") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + y + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_nested_defer_on_entities() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + me: User + } + + type User @key(fields: "id") { + id: ID! + name: String + } + "#, + Subgraph2: r#" + type User @key(fields: "id") { + id: ID! + messages: [Message] + } + + type Message @key(fields: "id") { + id: ID! + body: String + author: User + } + "#, + ); + + assert_plan!(planner, + r#" + { + me { + name + ... on User @defer { + messages { + body + author { + name + ... @defer { + messages { + body + } + } + } + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + me { + name + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + me { + __typename + name + id + } + } + }, + }, [ + Deferred(depends: [0], path: "me") { + Defer { + Primary { + { + ... on User { + messages { + body + author { + name + } + } + } + }: + Sequence { + Flatten(path: "me") { + Fetch(service: "Subgraph2", id: 1) { + { + ... on User { + __typename + id + } + } => + { + ... on User { + messages { + body + author { + __typename + id + } + } + } + } + }, + }, + Flatten(path: "me.messages.@.author") { + Fetch(service: "Subgraph1") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + name + } + } + }, + }, + }, + }, [ + Deferred(depends: [1], path: "me/messages/author") { + { + messages { + body + } + }: + Flatten(path: "me.messages.@.author") { + Fetch(service: "Subgraph2") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + messages { + body + } + } + } + }, + }, + }, + ] + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_on_value_types() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + me: User + } + + type User @key(fields: "id") { + id: ID! + name: String + } + "#, + Subgraph2: r#" + type User @key(fields: "id") { + id: ID! + messages: [Message] + } + + type Message { + id: ID! + body: MessageBody + } + + type MessageBody { + paragraphs: [String] + lines: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + me { + ... @defer { + messages { + ... @defer { + body { + lines + } + } + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + Fetch(service: "Subgraph1", id: 0) { + { + me { + __typename + id + } + } + }, + }, [ + Deferred(depends: [0], path: "me") { + Defer { + Primary { + Flatten(path: "me") { + Fetch(service: "Subgraph2") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + messages { + body { + lines + } + } + } + } + }, + }, + }, [ + Deferred(depends: [], path: "me/messages") { + { + body { + lines + } + }: + }, + ] + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_direct_nesting_on_entity() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + me: User + } + + type User @key(fields: "id") { + id: ID! + name: String + } + "#, + Subgraph2: r#" + type User @key(fields: "id") { + id: ID! + age: Int + address: String + } + "#, + ); + + assert_plan!(planner, + r#" + { + me { + name + ... @defer { + age + ... @defer { + address + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + me { + name + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + me { + __typename + name + id + } + } + }, + }, [ + Deferred(depends: [0], path: "me") { + Defer { + Primary { + { + age + }: + Flatten(path: "me") { + Fetch(service: "Subgraph2") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + age + } + } + }, + }, + }, [ + Deferred(depends: [0], path: "me") { + { + address + }: + Flatten(path: "me") { + Fetch(service: "Subgraph2") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + address + } + } + }, + }, + }, + ] + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_direct_nesting_on_value_type() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + me: User + } + + type User { + id: ID! + name: String + age: Int + address: String + } + "#, + ); + + assert_plan!(planner, + r#" + { + me { + name + ... @defer { + age + ... @defer { + address + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + me { + name + } + }: + Fetch(service: "Subgraph1") { + { + me { + name + age + address + } + } + }, + }, [ + Deferred(depends: [], path: "me") { + Defer { + Primary { + { + age + } + }, [ + Deferred(depends: [], path: "me") { + { + address + }: + }, + ] + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_on_enity_but_with_unuseful_key() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T { + id: ID! @shareable + a: Int + b: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + ... @defer { + a + ... @defer { + b + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + Fetch(service: "Subgraph1") { + { + t { + a + b + } + } + }, + }, [ + Deferred(depends: [], path: "t") { + Defer { + Primary { + { + a + } + }, [ + Deferred(depends: [], path: "t") { + { + b + }: + }, + ] + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_on_mutation_in_same_subgraph() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type Mutation { + update1: T + update2: T + } + + type T @key(fields: "id") { + id: ID! + v0: String + v1: String + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v2: String + } + "#, + ); + + // What matters here is that the updates (that go to different fields) are correctly done in sequence, + // and that defers have proper dependency set. + assert_plan!(planner, + r#" + mutation mut { + update1 { + v0 + ... @defer { + v1 + } + } + update2 { + v1 + ... @defer { + v0 + v2 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + update1 { + v0 + } + update2 { + v1 + } + }: + Fetch(service: "Subgraph1", id: 2) { + { + update1 { + __typename + v0 + id + } + update2 { + __typename + v1 + id + } + } + }, + }, [ + Deferred(depends: [2], path: "update1") { + { + v1 + }: + Flatten(path: "update1") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + }, + Deferred(depends: [2], path: "update2") { + { + v0 + v2 + }: + Parallel { + Flatten(path: "update2") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v0 + } + } + }, + }, + Flatten(path: "update2") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_on_mutation_on_different_subgraphs() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type Mutation { + update1: T + } + + type T @key(fields: "id") { + id: ID! + v0: String + v1: String + } + "#, + Subgraph2: r#" + type Mutation { + update2: T + } + + type T @key(fields: "id") { + id: ID! + v2: String + } + "#, + ); + + // What matters here is that the updates (that go to different fields) are correctly done in sequence, + // and that defers have proper dependency set. + assert_plan!(planner, + r#" + mutation mut { + update1 { + v0 + ... @defer { + v1 + } + } + update2 { + v1 + ... @defer { + v0 + v2 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + update1 { + v0 + } + update2 { + v1 + } + }: + Sequence { + Fetch(service: "Subgraph1", id: 0) { + { + update1 { + __typename + v0 + id + } + } + }, + Fetch(service: "Subgraph2", id: 1) { + { + update2 { + __typename + id + } + } + }, + Flatten(path: "update2") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + }, + }, [ + Deferred(depends: [0], path: "update1") { + { + v1 + }: + Flatten(path: "update1") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v1 + } + } + }, + }, + }, + Deferred(depends: [1], path: "update2") { + { + v0 + v2 + }: + Parallel { + Flatten(path: "update2") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v0 + } + } + }, + }, + Flatten(path: "update2") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_on_multi_dependency_deferred_section() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id0") { + id0: ID! + v1: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id0") @key(fields: "id1") { + id0: ID! + id1: ID! + v2: Int + } + "#, + Subgraph3: r#" + type T @key(fields: "id0") @key(fields: "id2") { + id0: ID! + id2: ID! + v3: Int + } + "#, + Subgraph4: r#" + type T @key(fields: "id1 id2") { + id1: ID! + id2: ID! + v4: Int + } + "#, + ); + + assert_plan!(&planner, + r#" + { + t { + v1 + v2 + v3 + ... @defer { + v4 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v1 + v2 + v3 + } + }: + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id0 + v1 + } + } + }, + Parallel { + Flatten(path: "t") { + Fetch(service: "Subgraph2", id: 0) { + { + ... on T { + __typename + id0 + } + } => + { + ... on T { + v2 + id1 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph3", id: 1) { + { + ... on T { + __typename + id0 + } + } => + { + ... on T { + v3 + id2 + } + } + }, + }, + }, + }, + }, [ + Deferred(depends: [0, 1], path: "t") { + { + v4 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph4") { + { + ... on T { + __typename + id1 + id2 + } + } => + { + ... on T { + v4 + } + } + }, + }, + }, + ] + }, + } + "### + ); + + // TODO: the following plan is admittedly not as effecient as it could be, as the 2 queries to + // subgraph 2 and 3 are done in the "primary" section, but all they do is handle transitive + // key dependencies for the deferred block, so it would make more sense to defer those fetches + // as well. It is however tricky to both improve this here _and_ maintain the plan generate + // just above (which is admittedly optimial). More precisely, what the code currently does is + // that when it gets to a defer, then it defers the fetch that gets the deferred fields (the + // fetch to subgraph 4 here), but it puts the "condition" resolution for the key of that fetch + // in the non-deferred section. Here, resolving that fetch conditions is what creates the + // dependency on the the fetches to subgraph 2 and 3, and so those get non-deferred. + // Now, it would be reasonably simple to say that when we resolve the "conditions" for a deferred + // fetch, then the first "hop" is non-deferred, but any following ones do get deferred, which + // would move the 2 fetches to subgraph 2 and 3 in the deferred section. The problem is that doing + // that wholesale means that in the previous example above, we'd keep the 2 non-deferred fetches + // to subgraph 2 and 3 for v2 and v3, but we would then have new deferred fetches to those + // subgraphs in the deferred section to now get the key id1 and id2, and that is in turn arguably + // non-optimal. So ideally, the code would be able to distinguish between those 2 cases and + // do the most optimal thing in each cases, but it's not that simple to do with the current + // code. + // Taking a step back, this "inefficiency" only exists where there is a @key "chain", and while + // such chains have their uses, they are likely pretty rare in the first place. And as the + // generated plan is not _that_ bad either, optimizing this feels fairly low priority and + // we leave it for "later". + assert_plan!(planner, + r#" + { + t { + v1 + ... @defer { + v4 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v1 + } + }: + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + v1 + id0 + } + } + }, + Parallel { + Flatten(path: "t") { + Fetch(service: "Subgraph2", id: 0) { + { + ... on T { + __typename + id0 + } + } => + { + ... on T { + id1 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph3", id: 1) { + { + ... on T { + __typename + id0 + } + } => + { + ... on T { + id2 + } + } + }, + }, + }, + }, + }, [ + Deferred(depends: [0, 1], path: "t") { + { + v4 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph4") { + { + ... on T { + __typename + id1 + id2 + } + } => + { + ... on T { + v4 + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_requirements_of_deferred_fields_are_deferred() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + v1: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v2: Int @requires(fields: "v3") + v3: Int @external + } + "#, + Subgraph3: r#" + type T @key(fields: "id") { + id: ID! + v3: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v1 + ... @defer { + v2 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v1 + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + v1 + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + v2 + }: + Sequence { + Flatten(path: "t") { + Fetch(service: "Subgraph3") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v3 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + v3 + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_provides_are_ignored_for_deferred_fields() { + // NOTE: this test tests the currently implemented behaviour, which ignore @provides when it + // concerns a deferred field. However, this is the behaviour implemented at the moment more + // because it is the simplest option and it's not illogical, but it is not the only possibly + // valid option. In particular, one could make the case that if a subgraph has a `@provides`, + // then this probably means that the subgraph can provide the field "cheaply" (why have + // a `@provides` otherwise?), and so that ignoring the @defer (instead of ignoring the @provides) + // is preferable. We can change to this behaviour later if we decide that it is preferable since + // the responses sent to the end-user would be the same regardless. + + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T @provides(fields: "v2") + } + + type T @key(fields: "id") { + id: ID! + v1: Int + v2: Int @external + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v2: Int @shareable + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v1 + ... @defer { + v2 + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v1 + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + v1 + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + v2 + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_on_query_root_type() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + op1: Int + op2: A + } + + type A { + x: Int + y: Int + next: Query + } + "#, + Subgraph2: r#" + type Query { + op3: Int + op4: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + op2 { + x + y + next { + op3 + ... @defer { + op1 + op4 + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + op2 { + x + y + next { + op3 + } + } + }: + Sequence { + Fetch(service: "Subgraph1", id: 0) { + { + op2 { + x + y + next { + __typename + } + } + } + }, + Flatten(path: "op2.next") { + Fetch(service: "Subgraph2") { + { + ... on Query { + op3 + } + } + }, + }, + }, + }, [ + Deferred(depends: [0], path: "op2/next") { + { + op1 + op4 + }: + Parallel { + Flatten(path: "op2.next") { + Fetch(service: "Subgraph1") { + { + ... on Query { + op1 + } + } + }, + }, + Flatten(path: "op2.next") { + Fetch(service: "Subgraph2") { + { + ... on Query { + op4 + } + } + }, + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_on_everything_queried() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + x: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + y: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + ... @defer { + t { + x + y + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary {}, [ + Deferred(depends: [], path: "") { + { + t { + x + y + } + }: + Sequence { + Flatten(path: "") { + Fetch(service: "Subgraph1") { + { + ... on Query { + t { + __typename + id + x + } + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_everything_within_entity() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + x: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + y: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + ... @defer { + x + y + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + x + y + }: + Parallel { + Flatten(path: "t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + x + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_with_conditions_and_labels() { + let planner = planner!(config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + x: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + y: Int + } + "#, + ); + + // without explicit label + assert_plan!(&planner, + r#" + query($cond: Boolean) { + t { + x + ... @defer(if: $cond) { + y + } + } + } + "#, + @r###" + QueryPlan { + Condition(if: $cond) { + Then { + Defer { + Primary { + { + t { + x + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + x + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + y + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + ] + }, + } Else { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + x + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + }, + }, + } + "### + ); + // with explicit label + assert_plan!(planner, + r#" + query($cond: Boolean) { + t { + x + ... @defer(label: "testLabel" if: $cond) { + y + } + } + } + "#, + @r###" + QueryPlan { + Condition(if: $cond) { + Then { + Defer { + Primary { + { + t { + x + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + x + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t", label: "testLabel") { + { + y + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + ] + }, + } Else { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + x + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + }, + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_with_condition_on_single_subgraph() { + // This test mostly serves to illustrate why we handle @defer conditions with `ConditionNode` instead of + // just generating only the plan with the @defer and ignoring the `DeferNode` at execution: this is + // because doing can result in sub-par execution for the case where the @defer is disabled (unless of + // course the execution "merges" fetch groups, but it's not trivial to do so). + + let planner = planner!(config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + x: Int + y: Int + } + "#, + ); + assert_plan!(planner, + r#" + query ($cond: Boolean) { + t { + x + ... @defer(if: $cond) { + y + } + } + } + "#, + @r###" + QueryPlan { + Condition(if: $cond) { + Then { + Defer { + Primary { + { + t { + x + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + x + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + y + }: + Flatten(path: "t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + ] + }, + } Else { + Fetch(service: "Subgraph1") { + { + t { + x + y + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_with_mutliple_conditions_and_labels() { + let planner = planner!(config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + x: Int + u: U + } + + type U @key(fields: "id") { + id: ID! + a: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + y: Int + } + "#, + Subgraph3: r#" + type U @key(fields: "id") { + id: ID! + b: Int + } + "#, + ); + assert_plan!(planner, + r#" + query ($cond1: Boolean, $cond2: Boolean) { + t { + x + ... @defer(if: $cond1, label: "foo") { + y + } + ... @defer(if: $cond2, label: "bar") { + u { + a + ... @defer(if: $cond1) { + b + } + } + } + } + } + "#, + @r###" + QueryPlan { + Condition(if: $cond1) { + Then { + Condition(if: $cond2) { + Then { + Defer { + Primary { + { + t { + x + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + x + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t", label: "bar") { + Defer { + Primary { + { + u { + a + } + }: + Flatten(path: "t") { + Fetch(service: "Subgraph1", id: 1) { + { + ... on T { + __typename + id + } + } => + { + ... on T { + u { + __typename + a + id + } + } + } + }, + }, + }, [ + Deferred(depends: [1], path: "t/u") { + { + b + }: + Flatten(path: "t.u") { + Fetch(service: "Subgraph3") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + b + } + } + }, + }, + }, + ] + }, + }, + Deferred(depends: [0], path: "t", label: "foo") { + { + y + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + ] + }, + } Else { + Defer { + Primary { + { + t { + x + u { + a + } + } + }: + Fetch(service: "Subgraph1", id: 2) { + { + t { + __typename + x + id + u { + __typename + a + id + } + } + } + }, + }, [ + Deferred(depends: [2], path: "t", label: "foo") { + { + y + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + Deferred(depends: [2], path: "t/u") { + { + b + }: + Flatten(path: "t.u") { + Fetch(service: "Subgraph3") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + b + } + } + }, + }, + }, + ] + }, + }, + }, + } Else { + Condition(if: $cond2) { + Then { + Defer { + Primary { + { + t { + x + y + } + }: + Sequence { + Fetch(service: "Subgraph1", id: 3) { + { + t { + __typename + id + x + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + }, [ + Deferred(depends: [3], path: "t", label: "bar") { + { + u { + a + b + } + }: + Sequence { + Flatten(path: "t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + u { + __typename + id + a + } + } + } + }, + }, + Flatten(path: "t.u") { + Fetch(service: "Subgraph3") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + b + } + } + }, + }, + }, + }, + ] + }, + } Else { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + x + u { + __typename + id + a + } + } + } + }, + Parallel { + Flatten(path: "t.u") { + Fetch(service: "Subgraph3") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + b + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + } + } + }, + }, + }, + }, + }, + }, + }, + }, + } + "### + ); +} + +#[test] +fn defer_test_interface_has_different_definitions_between_subgraphs() { + // This test exists to ensure an early bug is fixed: that bug was in the code building + // the `subselection` of `DeferNode` in the plan, and was such that those subselections + // were created with links to subgraph types instead the supergraph ones. As a result, + // we were sometimes trying to add a field (`b` in the example here) to version of a + // type that didn't had that field (the definition of `I` in Subgraph1 here), hence + // running into an assertion error. + + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + i: I + } + + interface I { + a: Int + c: Int + } + + type T implements I @key(fields: "id") { + id: ID! + a: Int + c: Int + } + "#, + Subgraph2: r#" + interface I { + b: Int + } + + type T implements I @key(fields: "id") { + id: ID! + a: Int @external + b: Int @requires(fields: "a") + } + "#, + ); + + assert_plan!(planner, + r#" + query Dimensions { + i { + a + b + ... @defer { + c + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + i { + a + ... on T { + b + } + } + }: + Sequence { + Fetch(service: "Subgraph1") { + { + i { + __typename + a + ... on T { + __typename + id + a + } + c + } + } + }, + Flatten(path: "i") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + a + } + } => + { + ... on T { + b + } + } + }, + }, + }, + }, [ + Deferred(depends: [], path: "i") { + { + c + }: + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_named_fragments_simple() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + x: Int + y: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + ...TestFragment @defer + } + } + + fragment TestFragment on T { + x + y + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + ... on T { + x + y + } + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + x + y + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_fragments_expand_into_same_field_regardless_of_defer() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + x: Int + y: Int + z: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + ...Fragment1 + ...Fragment2 @defer + } + } + + fragment Fragment1 on T { + x + y + } + + fragment Fragment2 on T { + y + z + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + x + y + } + }: + Sequence { + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + x + y + } + } + }, + }, + }, + }, [ + Deferred(depends: [0], path: "t") { + { + ... on T { + y + z + } + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + y + z + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_can_request_typename_in_fragment() { + // NOTE: There is nothing super special about __typename in theory, but because it's a field + // that is always available in all subghraph (for a type the subgraph has), it tends to create + // multiple options for the query planner, and so excercises some code-paths that triggered an + // early bug in the handling of `@defer` + // (https://github.com/apollographql/federation/issues/2128). + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + x: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + y: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + ...OnT @defer + x + } + } + + fragment OnT on T { + y + __typename + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + x + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + id + x + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + ... on T { + __typename + y + } + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + __typename + y + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_do_not_merge_query_branches_with_defer() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + a: Int + b: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + c: Int + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + a + ... @defer { + b + } + ... @defer { + c + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + a + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + t { + __typename + a + id + } + } + }, + }, [ + Deferred(depends: [0], path: "t") { + { + c + }: + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + c + } + } + }, + }, + }, + Deferred(depends: [0], path: "t") { + { + b + }: + Flatten(path: "t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + b + } + } + }, + }, + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_defer_only_the_key_of_an_entity() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + v0: String + } + "#, + ); + + assert_plan!(planner, + r#" + { + t { + v0 + ... @defer { + id + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + t { + v0 + } + }: + Fetch(service: "Subgraph1") { + { + t { + v0 + id + } + } + }, + }, [ + Deferred(depends: [], path: "t") { + { + id + }: + }, + ] + }, + } + "### + ); +} + +#[test] +fn defer_test_the_path_in_defer_includes_traversed_fragments() { + let planner = planner!( + config = config_with_defer(), + Subgraph1: r#" + type Query { + i: I + } + + interface I { + x: Int + } + + type A implements I { + x: Int + t: T + } + + type T @key(fields: "id") { + id: ID! + v1: String + v2: String + } + "#, + ); + + assert_plan!(planner, + r#" + { + i { + ... on A { + t { + v1 + ... @defer { + v2 + } + } + } + } + } + "#, + @r###" + QueryPlan { + Defer { + Primary { + { + i { + ... on A { + t { + v1 + } + } + } + }: + Fetch(service: "Subgraph1", id: 0) { + { + i { + __typename + ... on A { + t { + __typename + v1 + id + } + } + } + } + }, + }, [ + Deferred(depends: [0], path: "i/... on A/t") { + { + v2 + }: + Flatten(path: "i.t") { + Fetch(service: "Subgraph1") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + }, + ] + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs index 0cd50ce299..d15e05bf6f 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs @@ -790,7 +790,7 @@ fn it_handles_interface_object_input_rewrites_when_cloning_dependency_graph() { }, Parallel { Flatten(path: "i.i2") { - Fetch(service: "S4") { + Fetch(service: "S3") { { ... on T { __typename diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/requires/include_skip.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/requires/include_skip.rs index ebb98c6653..b5dad429d8 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/requires/include_skip.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/requires/include_skip.rs @@ -5,7 +5,7 @@ fn it_handles_a_simple_at_requires_triggered_within_a_conditional() { type Query { t: T } - + type T @key(fields: "id") { id: ID! a: Int @@ -71,7 +71,7 @@ fn it_handles_an_at_requires_triggered_conditionally() { type Query { t: T } - + type T @key(fields: "id") { id: ID! a: Int @@ -133,15 +133,13 @@ fn it_handles_an_at_requires_triggered_conditionally() { } #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure (redundant inline spread) fn it_handles_an_at_requires_where_multiple_conditional_are_involved() { let planner = planner!( Subgraph1: r#" type Query { a: A } - + type A @key(fields: "idA") { idA: ID! } @@ -151,7 +149,7 @@ fn it_handles_an_at_requires_where_multiple_conditional_are_involved() { idA: ID! b: [B] } - + type B @key(fields: "idB") { idB: ID! required: Int @@ -230,9 +228,9 @@ fn it_handles_an_at_requires_where_multiple_conditional_are_involved() { } }, }, - } + }, }, - } + }, }, } "### @@ -246,12 +244,10 @@ fn unnecessary_include_is_stripped_from_fragments() { type Query { foo: Foo, } - type Foo @key(fields: "id") { id: ID, bar: Bar, } - type Bar @key(fields: "id") { id: ID, } @@ -336,13 +332,11 @@ fn selections_are_not_overwritten_after_removing_directives() { type Query { foo: Foo, } - type Foo @key(fields: "id") { id: ID, foo: Foo, bar: Bar, } - type Bar @key(fields: "id") { id: ID, } diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/subscriptions.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/subscriptions.rs index 67c3367c31..b2bd3ed6fb 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/subscriptions.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/subscriptions.rs @@ -1,3 +1,5 @@ +use apollo_compiler::name; +use apollo_compiler::ExecutableDocument; use apollo_federation::query_plan::query_planner::QueryPlanIncrementalDeliveryConfig; use apollo_federation::query_plan::query_planner::QueryPlannerConfig; @@ -135,19 +137,13 @@ fn basic_subscription_with_single_subgraph() { ); } -// TODO(@TylerBloom): Currently, all defer directives are stripped out, so this does not panic -// quite as expected. Instead, it panics because the snapshots doesn't match. Once this behavior is -// changed, this should panic with an error along the lines of "@defer can't be used with -// subscriptions". #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: Subscription handling fn trying_to_use_defer_with_a_subcription_results_in_an_error() { let config = QueryPlannerConfig { incremental_delivery: QueryPlanIncrementalDeliveryConfig { enable_defer: true }, ..Default::default() }; - let planner = planner!( + let (api_schema, planner) = planner!( config = config, SubgraphA: r#" type Query { @@ -173,8 +169,9 @@ fn trying_to_use_defer_with_a_subcription_results_in_an_error() { address: String! } "#); - assert_plan!( - &planner, + + let document = ExecutableDocument::parse_and_validate( + api_schema.schema(), r#" subscription MySubscription { onNewUser { @@ -186,23 +183,11 @@ fn trying_to_use_defer_with_a_subcription_results_in_an_error() { } } "#, - // This is just a placeholder. We expect the planner to return an Err, which is then - // unwrapped. - @r###" - QueryPlan { - Subscription { - Primary: { - Fetch(service: "subgraphA") { - { - onNewUser { - id - name - } - } - } - }, - } - }, - "### - ); + "trying_to_use_defer_with_a_subcription_results_in_an_error.graphql", + ) + .unwrap(); + + planner + .build_query_plan(&document, Some(name!(MySubscription)), Default::default()) + .expect_err("should return an error"); } diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_can_request_typename_in_fragment.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_can_request_typename_in_fragment.graphql new file mode 100644 index 0000000000..20cd0d93cd --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_can_request_typename_in_fragment.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: e2543fc649c80a566b573ebfad36fc0f7458a3a4 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH1) + y: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_everything_within_entity.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_everything_within_entity.graphql new file mode 100644 index 0000000000..3fb06aff10 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_everything_within_entity.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: 428d657ed6389527be73c6ad949cd2fc4da01b20 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH1) + y: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_multiple_fields_in_different_subgraphs.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_multiple_fields_in_different_subgraphs.graphql new file mode 100644 index 0000000000..a03862f610 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_multiple_fields_in_different_subgraphs.graphql @@ -0,0 +1,67 @@ +# Composed from subgraphs with hash: 6de814e8aeb455e136ac8627284d67ef4806de57 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") + SUBGRAPH3 @join__graph(name: "Subgraph3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) + @join__type(graph: SUBGRAPH3) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") + @join__type(graph: SUBGRAPH3, key: "id") +{ + id: ID! + v0: String @join__field(graph: SUBGRAPH1) + v1: String @join__field(graph: SUBGRAPH1) + v2: String @join__field(graph: SUBGRAPH2) + v3: String @join__field(graph: SUBGRAPH3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_enity_but_with_unuseful_key.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_enity_but_with_unuseful_key.graphql new file mode 100644 index 0000000000..3467b225d6 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_enity_but_with_unuseful_key.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: e9d8806fbdfd92a23a7dd2af1e343ff3b6de4a7c +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + a: Int @join__field(graph: SUBGRAPH1) + b: Int @join__field(graph: SUBGRAPH1) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_everything_queried.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_everything_queried.graphql new file mode 100644 index 0000000000..3fb06aff10 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_everything_queried.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: 428d657ed6389527be73c6ad949cd2fc4da01b20 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH1) + y: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_multi_dependency_deferred_section.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_multi_dependency_deferred_section.graphql new file mode 100644 index 0000000000..987e5b30e3 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_multi_dependency_deferred_section.graphql @@ -0,0 +1,74 @@ +# Composed from subgraphs with hash: cb3bc5e47277ef2c4a364fa62067cfc2426974ec +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") + SUBGRAPH3 @join__graph(name: "Subgraph3", url: "none") + SUBGRAPH4 @join__graph(name: "Subgraph4", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) + @join__type(graph: SUBGRAPH3) + @join__type(graph: SUBGRAPH4) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id0") + @join__type(graph: SUBGRAPH2, key: "id0") + @join__type(graph: SUBGRAPH2, key: "id1") + @join__type(graph: SUBGRAPH3, key: "id0") + @join__type(graph: SUBGRAPH3, key: "id2") + @join__type(graph: SUBGRAPH4, key: "id1 id2") +{ + id0: ID! @join__field(graph: SUBGRAPH1) @join__field(graph: SUBGRAPH2) @join__field(graph: SUBGRAPH3) + v1: Int @join__field(graph: SUBGRAPH1) + id1: ID! @join__field(graph: SUBGRAPH2) @join__field(graph: SUBGRAPH4) + v2: Int @join__field(graph: SUBGRAPH2) + id2: ID! @join__field(graph: SUBGRAPH3) @join__field(graph: SUBGRAPH4) + v3: Int @join__field(graph: SUBGRAPH3) + v4: Int @join__field(graph: SUBGRAPH4) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_in_same_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_in_same_subgraph.graphql new file mode 100644 index 0000000000..e2f0483965 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_in_same_subgraph.graphql @@ -0,0 +1,71 @@ +# Composed from subgraphs with hash: e33e3ea89ff4340ccd53321764ac7f1a2684eb3f +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query + mutation: Mutation +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Mutation + @join__type(graph: SUBGRAPH1) +{ + update1: T + update2: T +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v0: String @join__field(graph: SUBGRAPH1) + v1: String @join__field(graph: SUBGRAPH1) + v2: String @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_on_different_subgraphs.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_on_different_subgraphs.graphql new file mode 100644 index 0000000000..b2e03a0693 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_on_different_subgraphs.graphql @@ -0,0 +1,72 @@ +# Composed from subgraphs with hash: 557c634741b7e9f2f4288edb43724e47f1983d19 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query + mutation: Mutation +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Mutation + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + update1: T @join__field(graph: SUBGRAPH1) + update2: T @join__field(graph: SUBGRAPH2) +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v0: String @join__field(graph: SUBGRAPH1) + v1: String @join__field(graph: SUBGRAPH1) + v2: String @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_query_root_type.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_query_root_type.graphql new file mode 100644 index 0000000000..74e78631de --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_query_root_type.graphql @@ -0,0 +1,64 @@ +# Composed from subgraphs with hash: 0a1dc62b0c2282030c10f0e0f777f635d6abd3ba +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type A + @join__type(graph: SUBGRAPH1) +{ + x: Int + y: Int + next: Query +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + op1: Int @join__field(graph: SUBGRAPH1) + op2: A @join__field(graph: SUBGRAPH1) + op3: Int @join__field(graph: SUBGRAPH2) + op4: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_value_types.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_value_types.graphql new file mode 100644 index 0000000000..6167ec55b4 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_value_types.graphql @@ -0,0 +1,76 @@ +# Composed from subgraphs with hash: 01170823ab6c07812976d0983a101d49a319af5c +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Message + @join__type(graph: SUBGRAPH2) +{ + id: ID! + body: MessageBody +} + +type MessageBody + @join__type(graph: SUBGRAPH2) +{ + paragraphs: [String] + lines: Int +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + me: User @join__field(graph: SUBGRAPH1) +} + +type User + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + name: String @join__field(graph: SUBGRAPH1) + messages: [Message] @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_only_the_key_of_an_entity.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_only_the_key_of_an_entity.graphql new file mode 100644 index 0000000000..f41a6d9198 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_only_the_key_of_an_entity.graphql @@ -0,0 +1,58 @@ +# Composed from subgraphs with hash: 176bb27a622184464209652e70141da92aeef370 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + t: T +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + v0: String +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_resuming_in_the_same_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_resuming_in_the_same_subgraph.graphql new file mode 100644 index 0000000000..33e9d9c2d9 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_resuming_in_the_same_subgraph.graphql @@ -0,0 +1,59 @@ +# Composed from subgraphs with hash: 0e99f2e41acb0f707744e78845a55271589146e6 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + t: T +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + v0: String + v1: String +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_condition_on_single_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_condition_on_single_subgraph.graphql new file mode 100644 index 0000000000..b185de90ba --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_condition_on_single_subgraph.graphql @@ -0,0 +1,59 @@ +# Composed from subgraphs with hash: 59e305d633b6e337422f6431c32cc58defa07302 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + t: T +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + x: Int + y: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_conditions_and_labels.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_conditions_and_labels.graphql new file mode 100644 index 0000000000..20cd0d93cd --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_conditions_and_labels.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: e2543fc649c80a566b573ebfad36fc0f7458a3a4 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH1) + y: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_mutliple_conditions_and_labels.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_mutliple_conditions_and_labels.graphql new file mode 100644 index 0000000000..1b7e7bdb70 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_mutliple_conditions_and_labels.graphql @@ -0,0 +1,74 @@ +# Composed from subgraphs with hash: 2135cbed03439b8658c0113e4663849c991e244b +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") + SUBGRAPH3 @join__graph(name: "Subgraph3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) + @join__type(graph: SUBGRAPH3) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH1) + u: U @join__field(graph: SUBGRAPH1) + y: Int @join__field(graph: SUBGRAPH2) +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH3, key: "id") +{ + id: ID! + a: Int @join__field(graph: SUBGRAPH1) + b: Int @join__field(graph: SUBGRAPH3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_entity.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_entity.graphql new file mode 100644 index 0000000000..004ed2cf9c --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_entity.graphql @@ -0,0 +1,63 @@ +# Composed from subgraphs with hash: 355f15f0f2699fd21731b0403286c513357860d3 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + me: User @join__field(graph: SUBGRAPH1) +} + +type User + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + name: String @join__field(graph: SUBGRAPH1) + age: Int @join__field(graph: SUBGRAPH2) + address: String @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_value_type.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_value_type.graphql new file mode 100644 index 0000000000..992f860b3a --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_value_type.graphql @@ -0,0 +1,60 @@ +# Composed from subgraphs with hash: f507628640adf8891453e78dbd4280a132706b11 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + me: User +} + +type User + @join__type(graph: SUBGRAPH1) +{ + id: ID! + name: String + age: Int + address: String +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_do_not_merge_query_branches_with_defer.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_do_not_merge_query_branches_with_defer.graphql new file mode 100644 index 0000000000..6beeae8fd0 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_do_not_merge_query_branches_with_defer.graphql @@ -0,0 +1,63 @@ +# Composed from subgraphs with hash: 6af2331e8c3844fbee6c3888b80b44f51cd5bd3d +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + a: Int @join__field(graph: SUBGRAPH1) + b: Int @join__field(graph: SUBGRAPH1) + c: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_fragments_expand_into_same_field_regardless_of_defer.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_fragments_expand_into_same_field_regardless_of_defer.graphql new file mode 100644 index 0000000000..c952a4d998 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_fragments_expand_into_same_field_regardless_of_defer.graphql @@ -0,0 +1,63 @@ +# Composed from subgraphs with hash: 5382aae137e16d1dfb9955e2a5118b49c75320ea +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH2) + y: Int @join__field(graph: SUBGRAPH2) + z: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_with_defer_enabled.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_with_defer_enabled.graphql new file mode 100644 index 0000000000..385d1b4566 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_with_defer_enabled.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: f7c59d88291d4be94f4c484dc849118a08361e69 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v1: Int @join__field(graph: SUBGRAPH2) + v2: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_without_defer_enabled.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_without_defer_enabled.graphql new file mode 100644 index 0000000000..385d1b4566 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_without_defer_enabled.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: f7c59d88291d4be94f4c484dc849118a08361e69 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v1: Int @join__field(graph: SUBGRAPH2) + v2: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_interface_has_different_definitions_between_subgraphs.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_interface_has_different_definitions_between_subgraphs.graphql new file mode 100644 index 0000000000..3fc0c2ca0c --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_interface_has_different_definitions_between_subgraphs.graphql @@ -0,0 +1,74 @@ +# Composed from subgraphs with hash: d280d3aae78ad7080c8df8b5a8982d70a4000a78 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +interface I + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + a: Int @join__field(graph: SUBGRAPH1) + c: Int @join__field(graph: SUBGRAPH1) + b: Int @join__field(graph: SUBGRAPH2) +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + i: I @join__field(graph: SUBGRAPH1) +} + +type T implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__implements(graph: SUBGRAPH2, interface: "I") + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + a: Int @join__field(graph: SUBGRAPH1) @join__field(graph: SUBGRAPH2, external: true) + c: Int @join__field(graph: SUBGRAPH1) + b: Int @join__field(graph: SUBGRAPH2, requires: "a") +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_multiple_non_nested_defer_plus_label_handling.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_multiple_non_nested_defer_plus_label_handling.graphql new file mode 100644 index 0000000000..e35ec30241 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_multiple_non_nested_defer_plus_label_handling.graphql @@ -0,0 +1,75 @@ +# Composed from subgraphs with hash: 09643fc5e0d3abcab8a0f25c0c1e4e16da4169a4 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") + SUBGRAPH3 @join__graph(name: "Subgraph3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) + @join__type(graph: SUBGRAPH3) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v0: String @join__field(graph: SUBGRAPH1) + v1: String @join__field(graph: SUBGRAPH1) + v2: String @join__field(graph: SUBGRAPH2) + v3: U @join__field(graph: SUBGRAPH2) +} + +type U + @join__type(graph: SUBGRAPH2, key: "id") + @join__type(graph: SUBGRAPH3, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH3) + y: Int @join__field(graph: SUBGRAPH3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_named_fragments_simple.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_named_fragments_simple.graphql new file mode 100644 index 0000000000..e8552cbd81 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_named_fragments_simple.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: 395eb0d8e844d7a54e82eec772f007fafcc37a8e +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH2) + y: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_nested_defer_on_entities.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_nested_defer_on_entities.graphql new file mode 100644 index 0000000000..10c0f0e1cd --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_nested_defer_on_entities.graphql @@ -0,0 +1,70 @@ +# Composed from subgraphs with hash: 1ddb9374f772bf962933f20e08543315e8df2b01 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Message + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + body: String + author: User +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + me: User @join__field(graph: SUBGRAPH1) +} + +type User + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + name: String @join__field(graph: SUBGRAPH1) + messages: [Message] @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_one.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_one.graphql new file mode 100644 index 0000000000..6f00c992a6 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_one.graphql @@ -0,0 +1,68 @@ +# Composed from subgraphs with hash: 3bee990c996344aa1eb3a7211e3f497fcbe5e1a6 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v: V @join__field(graph: SUBGRAPH2) +} + +type V + @join__type(graph: SUBGRAPH2) +{ + a: Int + b: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_three.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_three.graphql new file mode 100644 index 0000000000..b5cdcdd682 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_three.graphql @@ -0,0 +1,76 @@ +# Composed from subgraphs with hash: c6019a1bd80338506615bb1b60a44fc384586a47 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v: V @join__field(graph: SUBGRAPH2) +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + x: Int @join__field(graph: SUBGRAPH1) +} + +type V + @join__type(graph: SUBGRAPH2) +{ + a: Int + u: U +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_two.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_two.graphql new file mode 100644 index 0000000000..a132722729 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_two.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: fc9fa92848d4ab0e200dcfd09762db2e4281efae +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id", resolvable: false) + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v1: String @join__field(graph: SUBGRAPH1) + v2: String @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_false.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_false.graphql new file mode 100644 index 0000000000..385d1b4566 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_false.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: f7c59d88291d4be94f4c484dc849118a08361e69 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v1: Int @join__field(graph: SUBGRAPH2) + v2: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_true.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_true.graphql new file mode 100644 index 0000000000..385d1b4566 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_true.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: f7c59d88291d4be94f4c484dc849118a08361e69 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v1: Int @join__field(graph: SUBGRAPH2) + v2: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_provides_are_ignored_for_deferred_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_provides_are_ignored_for_deferred_fields.graphql new file mode 100644 index 0000000000..a18f8f7682 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_provides_are_ignored_for_deferred_fields.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: 39be4a27050623ad7a03d8ae9fed684d1e0f3088 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + t: T @join__field(graph: SUBGRAPH1, provides: "v2") +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v1: Int @join__field(graph: SUBGRAPH1) + v2: Int @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_requirements_of_deferred_fields_are_deferred.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_requirements_of_deferred_fields_are_deferred.graphql new file mode 100644 index 0000000000..0e2903ca92 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_requirements_of_deferred_fields_are_deferred.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: b3f138a89fd35476e9e36002ae4c8c1ab51c4530 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") + SUBGRAPH3 @join__graph(name: "Subgraph3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) + @join__type(graph: SUBGRAPH3) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") + @join__type(graph: SUBGRAPH3, key: "id") +{ + id: ID! + v1: Int @join__field(graph: SUBGRAPH1) + v2: Int @join__field(graph: SUBGRAPH2, requires: "v3") + v3: Int @join__field(graph: SUBGRAPH2, external: true) @join__field(graph: SUBGRAPH3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_the_path_in_defer_includes_traversed_fragments.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_the_path_in_defer_includes_traversed_fragments.graphql new file mode 100644 index 0000000000..82c2cfae78 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_the_path_in_defer_includes_traversed_fragments.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: eea1ddd3f3e944aaeb5f39f8f9018fd32bcdaaf6 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type A implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__type(graph: SUBGRAPH1) +{ + x: Int + t: T +} + +interface I + @join__type(graph: SUBGRAPH1) +{ + x: Int +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: SUBGRAPH1) +{ + i: I +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + v1: String + v2: String +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql index c7102b37a9..7cd49ac90f 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql @@ -1,4 +1,4 @@ -# Composed from subgraphs with hash: a95f4fdeb4fa54d3e87c7a5eb71952eba3efdf28 +# Composed from subgraphs with hash: 88de1465b4ef08a76a910cff26136f931b69eca4 schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql index c7102b37a9..7cd49ac90f 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql @@ -1,4 +1,4 @@ -# Composed from subgraphs with hash: a95f4fdeb4fa54d3e87c7a5eb71952eba3efdf28 +# Composed from subgraphs with hash: 88de1465b4ef08a76a910cff26136f931b69eca4 schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql index 1b13a0feb0..9442a08abf 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql @@ -1,4 +1,4 @@ -# Composed from subgraphs with hash: b43c835356ccf4afa94c6ad48206c70f22f39ffc +# Composed from subgraphs with hash: 213ee593775b6e4a22a852a35da688bc2e85d710 schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) diff --git a/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql b/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql index 6c750cf146..6d2137b561 100644 --- a/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql @@ -1,4 +1,4 @@ -# Composed from subgraphs with hash: a3e748206fa553b8f317912a3a411ee68b55db5d +# Composed from subgraphs with hash: 56ab651bf30bcb3ac7a9fb826fa6b03a57288859 schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) diff --git a/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql b/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql index a3668cb67b..33f5dd6f05 100644 --- a/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql @@ -1,4 +1,4 @@ -# Composed from subgraphs with hash: 133c359820bd42cb8afd64ab1e02bb912b5d1746 +# Composed from subgraphs with hash: e6c72fb53e93abe8f8aed4982aca6f6109fe1171 schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) diff --git a/apollo-router-benchmarks/Cargo.toml b/apollo-router-benchmarks/Cargo.toml index 38759d2feb..65114fead0 100644 --- a/apollo-router-benchmarks/Cargo.toml +++ b/apollo-router-benchmarks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-benchmarks" -version = "1.55.0" +version = "1.56.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/Cargo.toml b/apollo-router-scaffold/Cargo.toml index 4801a6c616..1f0b865959 100644 --- a/apollo-router-scaffold/Cargo.toml +++ b/apollo-router-scaffold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-scaffold" -version = "1.55.0" +version = "1.56.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/templates/base/Cargo.template.toml b/apollo-router-scaffold/templates/base/Cargo.template.toml index a1c3d2a42d..8dd36bf095 100644 --- a/apollo-router-scaffold/templates/base/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/Cargo.template.toml @@ -22,7 +22,7 @@ apollo-router = { path ="{{integration_test}}apollo-router" } apollo-router = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} # Note if you update these dependencies then also update xtask/Cargo.toml -apollo-router = "1.55.0" +apollo-router = "1.56.0" {{/if}} {{/if}} async-trait = "0.1.52" diff --git a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml index c566a3a0ca..b6d72bde79 100644 --- a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml @@ -13,7 +13,7 @@ apollo-router-scaffold = { path ="{{integration_test}}apollo-router-scaffold" } {{#if branch}} apollo-router-scaffold = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} -apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.55.0" } +apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.56.0" } {{/if}} {{/if}} anyhow = "1.0.58" diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 4675c9a5d7..fc48d928eb 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router" -version = "1.55.0" +version = "1.56.0" authors = ["Apollo Graph, Inc. "] repository = "https://github.com/apollographql/router/" documentation = "https://docs.rs/apollo-router" @@ -68,7 +68,7 @@ askama = "0.12.1" access-json = "0.1.0" anyhow = "1.0.86" apollo-compiler.workspace = true -apollo-federation = { path = "../apollo-federation", version = "=1.55.0" } +apollo-federation = { path = "../apollo-federation", version = "=1.56.0" } arc-swap = "1.6.0" async-channel = "1.9.0" async-compression = { version = "0.4.6", features = [ @@ -101,7 +101,6 @@ derive_more = { version = "0.99.17", default-features = false, features = [ ] } dhat = { version = "0.3.3", optional = true } diff = "0.1.13" -directories = "5.0.1" displaydoc = "0.2" flate2 = "1.0.30" fred = { version = "7.1.2", features = ["enable-rustls"] } @@ -197,7 +196,7 @@ regex = "1.10.5" reqwest.workspace = true # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.6.2+v2.9.1" +router-bridge = "=0.6.3+v2.9.2" rust-embed = { version = "8.4.0", features = ["include-exclude"] } rustls = "0.21.12" diff --git a/apollo-router/src/configuration/metrics.rs b/apollo-router/src/configuration/metrics.rs index 8e87fa74d0..b8bd132912 100644 --- a/apollo-router/src/configuration/metrics.rs +++ b/apollo-router/src/configuration/metrics.rs @@ -48,6 +48,7 @@ impl Metrics { data.populate_license_instrument(license_state); data.populate_user_plugins_instrument(configuration); data.populate_query_planner_experimental_parallelism(configuration); + data.populate_deno_or_rust_mode_instruments(configuration); data.into() } } @@ -532,6 +533,36 @@ impl InstrumentData { ); } } + + /// Populate metrics on the rollout of experimental Rust replacements of JavaScript code. + pub(crate) fn populate_deno_or_rust_mode_instruments(&mut self, configuration: &Configuration) { + let experimental_query_planner_mode = match configuration.experimental_query_planner_mode { + super::QueryPlannerMode::Legacy => "legacy", + super::QueryPlannerMode::Both => "both", + super::QueryPlannerMode::BothBestEffort => "both_best_effort", + super::QueryPlannerMode::New => "new", + }; + let experimental_introspection_mode = match configuration.experimental_introspection_mode { + super::IntrospectionMode::Legacy => "legacy", + super::IntrospectionMode::Both => "both", + super::IntrospectionMode::New => "new", + }; + + self.data.insert( + "apollo.router.config.experimental_query_planner_mode".to_string(), + ( + 1, + HashMap::from_iter([("mode".to_string(), experimental_query_planner_mode.into())]), + ), + ); + self.data.insert( + "apollo.router.config.experimental_introspection_mode".to_string(), + ( + 1, + HashMap::from_iter([("mode".to_string(), experimental_introspection_mode.into())]), + ), + ); + } } impl From for Metrics { @@ -564,6 +595,8 @@ mod test { use crate::configuration::metrics::InstrumentData; use crate::configuration::metrics::Metrics; + use crate::configuration::IntrospectionMode; + use crate::configuration::QueryPlannerMode; use crate::uplink::license_enforcement::LicenseState; use crate::Configuration; @@ -638,4 +671,40 @@ mod test { let _metrics: Metrics = data.into(); assert_non_zero_metrics_snapshot!(); } + + #[test] + fn test_experimental_mode_metrics() { + let mut data = InstrumentData::default(); + data.populate_deno_or_rust_mode_instruments(&Configuration { + experimental_introspection_mode: IntrospectionMode::Legacy, + experimental_query_planner_mode: QueryPlannerMode::Both, + ..Default::default() + }); + let _metrics: Metrics = data.into(); + assert_non_zero_metrics_snapshot!(); + } + + #[test] + fn test_experimental_mode_metrics_2() { + let mut data = InstrumentData::default(); + // Default query planner value should still be reported + data.populate_deno_or_rust_mode_instruments(&Configuration { + experimental_introspection_mode: IntrospectionMode::New, + ..Default::default() + }); + let _metrics: Metrics = data.into(); + assert_non_zero_metrics_snapshot!(); + } + + #[test] + fn test_experimental_mode_metrics_3() { + let mut data = InstrumentData::default(); + data.populate_deno_or_rust_mode_instruments(&Configuration { + experimental_introspection_mode: IntrospectionMode::New, + experimental_query_planner_mode: QueryPlannerMode::New, + ..Default::default() + }); + let _metrics: Metrics = data.into(); + assert_non_zero_metrics_snapshot!(); + } } diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics.snap new file mode 100644 index 0000000000..3bd3c58289 --- /dev/null +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/configuration/metrics.rs +expression: "&metrics.non_zero()" +--- +- name: apollo.router.config.experimental_introspection_mode + data: + datapoints: + - value: 1 + attributes: + mode: legacy +- name: apollo.router.config.experimental_query_planner_mode + data: + datapoints: + - value: 1 + attributes: + mode: both diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_2.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_2.snap new file mode 100644 index 0000000000..660542eeba --- /dev/null +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_2.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/configuration/metrics.rs +expression: "&metrics.non_zero()" +--- +- name: apollo.router.config.experimental_introspection_mode + data: + datapoints: + - value: 1 + attributes: + mode: new +- name: apollo.router.config.experimental_query_planner_mode + data: + datapoints: + - value: 1 + attributes: + mode: both_best_effort diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_3.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_3.snap new file mode 100644 index 0000000000..ba8bdf43af --- /dev/null +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__experimental_mode_metrics_3.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/configuration/metrics.rs +expression: "&metrics.non_zero()" +--- +- name: apollo.router.config.experimental_introspection_mode + data: + datapoints: + - value: 1 + attributes: + mode: new +- name: apollo.router.config.experimental_query_planner_mode + data: + datapoints: + - value: 1 + attributes: + mode: new diff --git a/apollo-router/src/executable.rs b/apollo-router/src/executable.rs index 4d826b6554..86bdee162f 100644 --- a/apollo-router/src/executable.rs +++ b/apollo-router/src/executable.rs @@ -4,6 +4,8 @@ use std::cell::Cell; use std::env; use std::fmt::Debug; use std::net::SocketAddr; +#[cfg(unix)] +use std::os::unix::fs::MetadataExt; use std::path::PathBuf; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; @@ -211,6 +213,11 @@ pub struct Opt { #[clap(skip = std::env::var("APOLLO_KEY").ok())] apollo_key: Option, + /// Key file location relative to the current directory. + #[cfg(unix)] + #[clap(long = "apollo-key-path", env = "APOLLO_KEY_PATH")] + apollo_key_path: Option, + /// Your Apollo graph reference. #[clap(skip = std::env::var("APOLLO_GRAPH_REF").ok())] apollo_graph_ref: Option, @@ -513,14 +520,19 @@ impl Executable { // 2. Env APOLLO_ROUTER_SUPERGRAPH_PATH // 3. Env APOLLO_ROUTER_SUPERGRAPH_URLS // 4. Env APOLLO_KEY and APOLLO_GRAPH_REF - let schema_source = match (schema, &opt.supergraph_path, &opt.supergraph_urls, &opt.apollo_key) { - (Some(_), Some(_), _, _) | (Some(_), _, Some(_), _) => { + #[cfg(unix)] + let akp = &opt.apollo_key_path; + #[cfg(not(unix))] + let akp: &Option = &None; + + let schema_source = match (schema, &opt.supergraph_path, &opt.supergraph_urls, &opt.apollo_key, akp) { + (Some(_), Some(_), _, _, _) | (Some(_), _, Some(_), _, _) => { return Err(anyhow!( "--supergraph and APOLLO_ROUTER_SUPERGRAPH_PATH cannot be used when a custom schema source is in use" )) } - (Some(source), None, None,_) => source, - (_, Some(supergraph_path), _, _) => { + (Some(source), None, None,_,_) => source, + (_, Some(supergraph_path), _, _, _) => { tracing::info!("{apollo_router_msg}"); tracing::info!("{apollo_telemetry_msg}"); @@ -535,7 +547,7 @@ impl Executable { delay: None, } } - (_, _, Some(supergraph_urls), _) => { + (_, _, Some(supergraph_urls), _, _) => { tracing::info!("{apollo_router_msg}"); tracing::info!("{apollo_telemetry_msg}"); @@ -545,7 +557,67 @@ impl Executable { period: opt.apollo_uplink_poll_interval } } - (_, None, None, Some(_apollo_key)) => { + (_, None, None, _, Some(apollo_key_path)) => { + let apollo_key_path = if apollo_key_path.is_relative() { + current_directory.join(apollo_key_path) + } else { + apollo_key_path.clone() + }; + + if !apollo_key_path.exists() { + tracing::error!( + "Apollo key at path '{}' does not exist.", + apollo_key_path.to_string_lossy() + ); + return Err(anyhow!( + "Apollo key at path '{}' does not exist.", + apollo_key_path.to_string_lossy() + )); + } else { + // On unix systems, Check that the executing user is the only user who may + // read the key file. + // Note: We could, in future, add support for Windows. + #[cfg(unix)] + { + let meta = std::fs::metadata(apollo_key_path.clone()).map_err(|err| + anyhow!( + "Failed to read Apollo key file: {}", + err + ))?; + let mode = meta.mode(); + // If our mode isn't "safe", fail... + // safe == none of the "group" or "other" bits set. + if mode & 0o077 != 0 { + return Err( + anyhow!( + "Apollo key file permissions ({:#o}) are too permissive", mode & 0o000777 + )); + } + let euid = unsafe { libc::geteuid() }; + let owner = meta.uid(); + if euid != owner { + return Err( + anyhow!( + "Apollo key file owner id ({owner}) does not match effective user id ({euid})" + )); + } + } + //The key file exists try and load it + match std::fs::read_to_string(&apollo_key_path) { + Ok(apollo_key) => { + opt.apollo_key = Some(apollo_key.trim().to_string()); + } + Err(err) => { + return Err(anyhow!( + "Failed to read Apollo key file: {}", + err + )); + } + }; + SchemaSource::Registry(opt.uplink_config()?) + } + } + (_, None, None, Some(_apollo_key), None) => { tracing::info!("{apollo_router_msg}"); tracing::info!("{apollo_telemetry_msg}"); SchemaSource::Registry(opt.uplink_config()?) @@ -585,7 +657,7 @@ impl Executable { } }; - // Order of precedence: + // Order of precedence for licenses: // 1. explicit path from cli // 2. env APOLLO_ROUTER_LICENSE // 3. uplink diff --git a/apollo-router/src/plugins/authorization/policy.rs b/apollo-router/src/plugins/authorization/policy.rs index 821546a01c..e317b7eb97 100644 --- a/apollo-router/src/plugins/authorization/policy.rs +++ b/apollo-router/src/plugins/authorization/policy.rs @@ -111,7 +111,7 @@ fn policy_argument( opt_directive: Option<&impl AsRef>, ) -> impl Iterator + '_ { opt_directive - .and_then(|directive| directive.as_ref().argument_by_name("policies")) + .and_then(|directive| directive.as_ref().specified_argument_by_name("policies")) // outer array .and_then(|value| value.as_list()) .into_iter() @@ -205,7 +205,7 @@ fn policies_sets_argument( directive: &ast::Directive, ) -> impl Iterator> + '_ { directive - .argument_by_name("policies") + .specified_argument_by_name("policies") // outer array .and_then(|value| value.as_list()) .into_iter() diff --git a/apollo-router/src/plugins/authorization/scopes.rs b/apollo-router/src/plugins/authorization/scopes.rs index 55a4d0a0c4..6dcccfc0a0 100644 --- a/apollo-router/src/plugins/authorization/scopes.rs +++ b/apollo-router/src/plugins/authorization/scopes.rs @@ -111,7 +111,7 @@ fn scopes_argument( opt_directive: Option<&impl AsRef>, ) -> impl Iterator + '_ { opt_directive - .and_then(|directive| directive.as_ref().argument_by_name("scopes")) + .and_then(|directive| directive.as_ref().specified_argument_by_name("scopes")) // outer array .and_then(|value| value.as_list()) .into_iter() @@ -188,7 +188,7 @@ impl<'a> traverse::Visitor for ScopeExtractionVisitor<'a> { fn scopes_sets_argument(directive: &ast::Directive) -> impl Iterator> + '_ { directive - .argument_by_name("scopes") + .specified_argument_by_name("scopes") // outer array .and_then(|value| value.as_list()) .into_iter() diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs b/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs index 6f23fcdc00..cf819478e1 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs @@ -90,7 +90,7 @@ impl CostDirective { .get(&COST_DIRECTIVE_NAME) .and_then(|name| directives.get(name)) .or(directives.get(&COST_DIRECTIVE_DEFAULT_NAME)) - .and_then(|cost| cost.argument_by_name(&COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME)) + .and_then(|cost| cost.specified_argument_by_name(&COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME)) .and_then(|weight| weight.to_i32()) .map(|weight| Self { weight }) } @@ -103,7 +103,7 @@ impl CostDirective { .get(&COST_DIRECTIVE_NAME) .and_then(|name| directives.get(name)) .or(directives.get(&COST_DIRECTIVE_DEFAULT_NAME)) - .and_then(|cost| cost.argument_by_name(&COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME)) + .and_then(|cost| cost.specified_argument_by_name(&COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME)) .and_then(|weight| weight.to_i32()) .map(|weight| Self { weight }) } @@ -120,7 +120,7 @@ impl IncludeDirective { let directive = field .directives .get("include") - .and_then(|skip| skip.argument_by_name("if")) + .and_then(|skip| skip.specified_argument_by_name("if")) .and_then(|arg| arg.to_bool()) .map(|cond| Self { is_included: cond }); @@ -167,10 +167,10 @@ impl DefinitionListSizeDirective { .or(definition.directives.get(&LIST_SIZE_DIRECTIVE_DEFAULT_NAME)); if let Some(directive) = directive { let assumed_size = directive - .argument_by_name(&LIST_SIZE_DIRECTIVE_ASSUMED_SIZE_ARGUMENT_NAME) + .specified_argument_by_name(&LIST_SIZE_DIRECTIVE_ASSUMED_SIZE_ARGUMENT_NAME) .and_then(|arg| arg.to_i32()); let slicing_argument_names = directive - .argument_by_name(&LIST_SIZE_DIRECTIVE_SLICING_ARGUMENTS_ARGUMENT_NAME) + .specified_argument_by_name(&LIST_SIZE_DIRECTIVE_SLICING_ARGUMENTS_ARGUMENT_NAME) .and_then(|arg| arg.as_list()) .map(|arg_list| { arg_list @@ -180,7 +180,7 @@ impl DefinitionListSizeDirective { .collect() }); let sized_fields = directive - .argument_by_name(&LIST_SIZE_DIRECTIVE_SIZED_FIELDS_ARGUMENT_NAME) + .specified_argument_by_name(&LIST_SIZE_DIRECTIVE_SIZED_FIELDS_ARGUMENT_NAME) .and_then(|arg| arg.as_list()) .map(|arg_list| { arg_list @@ -190,7 +190,9 @@ impl DefinitionListSizeDirective { .collect() }); let require_one_slicing_argument = directive - .argument_by_name(&LIST_SIZE_DIRECTIVE_REQUIRE_ONE_SLICING_ARGUMENT_ARGUMENT_NAME) + .specified_argument_by_name( + &LIST_SIZE_DIRECTIVE_REQUIRE_ONE_SLICING_ARGUMENT_ARGUMENT_NAME, + ) .and_then(|arg| arg.to_bool()) .unwrap_or(true); @@ -275,7 +277,7 @@ impl RequiresDirective { let requires_arg = definition .directives .get("join__field") - .and_then(|requires| requires.argument_by_name("requires")) + .and_then(|requires| requires.specified_argument_by_name("requires")) .and_then(|arg| arg.as_str()); if let Some(arg) = requires_arg { @@ -302,7 +304,7 @@ impl SkipDirective { let directive = field .directives .get("skip") - .and_then(|skip| skip.argument_by_name("if")) + .and_then(|skip| skip.specified_argument_by_name("if")) .and_then(|arg| arg.to_bool()) .map(|cond| Self { is_skipped: cond }); diff --git a/apollo-router/src/plugins/progressive_override/mod.rs b/apollo-router/src/plugins/progressive_override/mod.rs index 542b0d0722..d4e1adb9b5 100644 --- a/apollo-router/src/plugins/progressive_override/mod.rs +++ b/apollo-router/src/plugins/progressive_override/mod.rs @@ -91,7 +91,7 @@ fn collect_labels_from_schema(schema: &Schema) -> LabelsFromSchema { .flatten() .filter_map(|join_directive| { if let Some(override_label_arg) = - join_directive.argument_by_name(OVERRIDE_LABEL_ARG_NAME) + join_directive.specified_argument_by_name(OVERRIDE_LABEL_ARG_NAME) { override_label_arg .as_str() diff --git a/apollo-router/src/plugins/telemetry/config_new/events.rs b/apollo-router/src/plugins/telemetry/config_new/events.rs index e3bbd668f1..a58264bfb7 100644 --- a/apollo-router/src/plugins/telemetry/config_new/events.rs +++ b/apollo-router/src/plugins/telemetry/config_new/events.rs @@ -9,6 +9,7 @@ use parking_lot::Mutex; use schemars::JsonSchema; use serde::Deserialize; use tower::BoxError; +use tracing::info_span; use tracing::Span; use super::instruments::Instrumented; @@ -738,7 +739,10 @@ where let mut new_attributes = selectors.on_response_event(response, ctx); attributes.append(&mut new_attributes); } - + // Stub span to make sure the custom attributes are saved in current span extensions + // It won't be extracted or sampled at all + let span = info_span!("supergraph_event_send_event"); + let _entered = span.enter(); inner.send_event(attributes); } diff --git a/apollo-router/src/plugins/telemetry/dynamic_attribute.rs b/apollo-router/src/plugins/telemetry/dynamic_attribute.rs index 0b4af0964d..d9cde3ca21 100644 --- a/apollo-router/src/plugins/telemetry/dynamic_attribute.rs +++ b/apollo-router/src/plugins/telemetry/dynamic_attribute.rs @@ -240,7 +240,9 @@ impl EventDynAttribute for ::tracing::Span { self.with_subscriber(move |(id, dispatch)| { if let Some(reg) = dispatch.downcast_ref::() { match reg.span(id) { - None => eprintln!("no spanref, this is a bug"), + None => { + eprintln!("no spanref, this is a bug"); + } Some(s) => { if s.is_sampled() { let mut extensions = s.extensions_mut(); diff --git a/apollo-router/src/plugins/telemetry/tracing/jaeger.rs b/apollo-router/src/plugins/telemetry/tracing/jaeger.rs index 13f61da5c4..f50aebefc2 100644 --- a/apollo-router/src/plugins/telemetry/tracing/jaeger.rs +++ b/apollo-router/src/plugins/telemetry/tracing/jaeger.rs @@ -139,7 +139,8 @@ impl TracingConfigurator for Config { Ok(builder.with_span_processor( BatchSpanProcessor::builder(exporter, runtime::Tokio) .with_batch_config(batch_processor.clone().into()) - .build(), + .build() + .filtered(), )) } _ => Ok(builder), diff --git a/apollo-router/src/query_planner/dual_introspection.rs b/apollo-router/src/query_planner/dual_introspection.rs index a6c51d7f63..400de5dbb6 100644 --- a/apollo-router/src/query_planner/dual_introspection.rs +++ b/apollo-router/src/query_planner/dual_introspection.rs @@ -1,5 +1,6 @@ use std::cmp::Ordering; +use apollo_compiler::ast; use serde_json_bytes::Value; use crate::error::QueryPlannerError; @@ -27,16 +28,16 @@ pub(crate) fn compare_introspection_responses( if let (Some(js_data), Some(rust_data)) = (&mut js_response.data, &mut rust_response.data) { - json_sort_arrays(js_data); - json_sort_arrays(rust_data); + normalize_response(js_data); + normalize_response(rust_data); } is_matched = js_response.data == rust_response.data; if is_matched { - tracing::debug!("Introspection match! 🎉") + tracing::trace!("Introspection match! 🎉") } else { tracing::debug!("Introspection mismatch"); tracing::trace!("Introspection query:\n{query}"); - tracing::trace!("Introspection diff:\n{}", { + tracing::debug!("Introspection diff:\n{}", { let rust = rust_response .data .as_ref() @@ -67,23 +68,72 @@ pub(crate) fn compare_introspection_responses( ); } -fn json_sort_arrays(value: &mut Value) { +fn normalize_response(value: &mut Value) { match value { Value::Array(array) => { for item in array.iter_mut() { - json_sort_arrays(item) + normalize_response(item) } array.sort_by(json_compare) } Value::Object(object) => { - for (_key, value) in object { - json_sort_arrays(value) + for (key, value) in object { + if let Some(new_value) = normalize_default_value(key.as_str(), value) { + *value = new_value + } else { + normalize_response(value) + } } } Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {} } } +/// When a default value is an input object, graphql-js seems to sort its fields by name +fn normalize_default_value(key: &str, value: &Value) -> Option { + if key != "defaultValue" { + return None; + } + let default_value = value.as_str()?; + // We don’t have a parser entry point for a standalone GraphQL `Value`, + // so mint a document that contains that value. + let doc = format!("{{ field(arg: {default_value}) }}"); + let doc = ast::Document::parse(doc, "").ok()?; + let parsed_default_value = &doc + .definitions + .first()? + .as_operation_definition()? + .selection_set + .first()? + .as_field()? + .arguments + .first()? + .value; + parsed_default_value.as_object()?; + let normalized = normalize_parsed_default_value(parsed_default_value); + Some(normalized.serialize().no_indent().to_string().into()) +} + +fn normalize_parsed_default_value(value: &ast::Value) -> ast::Value { + match value { + ast::Value::List(items) => ast::Value::List( + items + .iter() + .map(|item| normalize_parsed_default_value(item).into()) + .collect(), + ), + ast::Value::Object(fields) => { + let mut new_fields: Vec<_> = fields + .iter() + .map(|(name, value)| (name.clone(), normalize_parsed_default_value(value).into())) + .collect(); + new_fields.sort_by(|(name_1, _value_1), (name_2, _value_2)| name_1.cmp(name_2)); + ast::Value::Object(new_fields) + } + v => v.clone(), + } +} + fn json_compare(a: &Value, b: &Value) -> Ordering { match (a, b) { (Value::Null, Value::Null) => Ordering::Equal, diff --git a/apollo-router/src/query_planner/dual_query_planner.rs b/apollo-router/src/query_planner/dual_query_planner.rs index 9952eb9782..91d273fb26 100644 --- a/apollo-router/src/query_planner/dual_query_planner.rs +++ b/apollo-router/src/query_planner/dual_query_planner.rs @@ -153,7 +153,7 @@ impl BothModeComparisonJob { let match_result = opt_plan_node_matches(js_root_node, &rust_root_node); is_matched = match_result.is_ok(); match match_result { - Ok(_) => tracing::debug!("JS and Rust query plans match{operation_desc}! 🎉"), + Ok(_) => tracing::trace!("JS and Rust query plans match{operation_desc}! 🎉"), Err(err) => { tracing::debug!("JS v.s. Rust query plan mismatch{operation_desc}"); tracing::debug!("{}", err.full_description()); @@ -386,7 +386,7 @@ fn vec_matches_result( item_matches(this, other) .map_err(|err| err.add_description(&format!("under item[{}]", index))) })?; - assert!(vec_matches(this, other, |a, b| item_matches(a, b).is_ok())); + assert!(vec_matches(this, other, |a, b| item_matches(a, b).is_ok())); // Note: looks redundant Ok(()) } @@ -402,12 +402,16 @@ fn vec_matches_sorted_by( this: &[T], other: &[T], compare: impl Fn(&T, &T) -> std::cmp::Ordering, -) -> bool { + item_matches: impl Fn(&T, &T) -> Result<(), MatchFailure>, +) -> Result<(), MatchFailure> { + check_match_eq!(this.len(), other.len()); let mut this_sorted = this.to_owned(); let mut other_sorted = other.to_owned(); this_sorted.sort_by(&compare); other_sorted.sort_by(&compare); - vec_matches(&this_sorted, &other_sorted, T::eq) + std::iter::zip(&this_sorted, &other_sorted) + .try_fold((), |_acc, (this, other)| item_matches(this, other))?; + Ok(()) } // performs a set comparison, ignoring order @@ -701,9 +705,13 @@ fn same_ast_operation_definition( ) -> Result<(), MatchFailure> { // Note: Operation names are ignored, since parallel fetches may have different names. check_match_eq!(x.operation_type, y.operation_type); - check_match!(vec_matches_sorted_by(&x.variables, &y.variables, |x, y| x - .name - .cmp(&y.name))); + vec_matches_sorted_by( + &x.variables, + &y.variables, + |a, b| a.name.cmp(&b.name), + |a, b| same_variable_definition(a, b), + ) + .map_err(|err| err.add_description("under Variable definition"))?; check_match_eq!(x.directives, y.directives); check_match!(same_ast_selection_set_sorted( &x.selection_set, @@ -712,6 +720,49 @@ fn same_ast_operation_definition( Ok(()) } +// Use this function, instead of `VariableDefinition`'s `PartialEq` implementation, +// due to known differences. +fn same_variable_definition( + x: &ast::VariableDefinition, + y: &ast::VariableDefinition, +) -> Result<(), MatchFailure> { + check_match_eq!(x.name, y.name); + check_match_eq!(x.ty, y.ty); + if x.default_value != y.default_value { + if let (Some(x), Some(y)) = (&x.default_value, &y.default_value) { + match (x.as_ref(), y.as_ref()) { + // Special case 1: JS QP may convert an enum value into string. + // - In this case, compare them as strings. + (ast::Value::String(ref x), ast::Value::Enum(ref y)) => { + if x == y.as_str() { + return Ok(()); + } + } + + // Special case 2: Rust QP expands an empty object value by filling in its + // default field values. + // - If the JS QP value is an empty object, consider any object is a match. + // - Assuming the Rust QP object value has only default field values. + // - Warning: This is an unsound heuristic. + (ast::Value::Object(ref x), ast::Value::Object(_)) => { + if x.is_empty() { + return Ok(()); + } + } + + _ => {} // otherwise, fall through + } + } + + return Err(MatchFailure::new(format!( + "mismatch between default values:\nleft: {:?}\nright: {:?}", + x.default_value, y.default_value + ))); + } + check_match_eq!(x.directives, y.directives); + Ok(()) +} + fn same_ast_fragment_definition( x: &ast::FragmentDefinition, y: &ast::FragmentDefinition, @@ -806,6 +857,28 @@ mod ast_comparison_tests { assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); } + #[test] + fn test_query_variable_decl_enum_value_coercion() { + // Note: JS QP converts enum default values into strings. + let op_x = r#"query($qv1: E! = "default_value") { x(arg1: $qv1) }"#; + let op_y = r#"query($qv1: E! = default_value) { x(arg1: $qv1) }"#; + let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); + let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); + assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); + } + + #[test] + fn test_query_variable_decl_object_value_coercion() { + // Note: Rust QP expands empty object default values by filling in its default field + // values. + let op_x = r#"query($qv1: T! = {}) { x(arg1: $qv1) }"#; + let op_y = + r#"query($qv1: T! = { field1: true, field2: "default_value" }) { x(arg1: $qv1) }"#; + let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); + let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); + assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); + } + #[test] fn test_entities_selection_order() { let op_x = r#" diff --git a/apollo-router/src/spec/query/change.rs b/apollo-router/src/spec/query/change.rs index 9db9554a04..8bca0e025b 100644 --- a/apollo-router/src/spec/query/change.rs +++ b/apollo-router/src/spec/query/change.rs @@ -286,7 +286,10 @@ impl<'a> QueryHashVisitor<'a> { fn hash_join_type(&mut self, name: &Name, directives: &DirectiveList) -> Result<(), BoxError> { if let Some(dir_name) = self.join_type_directive_name.as_deref() { if let Some(dir) = directives.get(dir_name) { - if let Some(key) = dir.argument_by_name("key").and_then(|arg| arg.as_str()) { + if let Some(key) = dir + .specified_argument_by_name("key") + .and_then(|arg| arg.as_str()) + { let mut parser = Parser::new(); if let Ok(field_set) = parser.parse_field_set( Valid::assume_valid_ref(self.schema), @@ -315,7 +318,7 @@ impl<'a> QueryHashVisitor<'a> { if let Some(dir_name) = self.join_field_directive_name.as_deref() { if let Some(dir) = directives.get(dir_name) { if let Some(requires) = dir - .argument_by_name("requires") + .specified_argument_by_name("requires") .and_then(|arg| arg.as_str()) { if let Ok(parent_type) = Name::new(parent_type) { diff --git a/apollo-router/src/spec/schema.rs b/apollo-router/src/spec/schema.rs index edb6c3bbac..2208a5863e 100644 --- a/apollo-router/src/spec/schema.rs +++ b/apollo-router/src/spec/schema.rs @@ -67,8 +67,10 @@ impl Schema { if let Some(join_enum) = definitions.get_enum("join__Graph") { for (name, url) in join_enum.values.iter().filter_map(|(_name, value)| { let join_directive = value.directives.get("join__graph")?; - let name = join_directive.argument_by_name("name")?.as_str()?; - let url = join_directive.argument_by_name("url")?.as_str()?; + let name = join_directive + .specified_argument_by_name("name")? + .as_str()?; + let url = join_directive.specified_argument_by_name("url")?.as_str()?; Some((name, url)) }) { if url.is_empty() { @@ -220,7 +222,7 @@ impl Schema { for directive in &self.supergraph_schema().schema_definition.directives { let join_url = if directive.name == "core" { let Some(feature) = directive - .argument_by_name("feature") + .specified_argument_by_name("feature") .and_then(|value| value.as_str()) else { continue; @@ -229,7 +231,7 @@ impl Schema { feature } else if directive.name == "link" { let Some(url) = directive - .argument_by_name("url") + .specified_argument_by_name("url") .and_then(|value| value.as_str()) else { continue; @@ -257,7 +259,7 @@ impl Schema { .filter(|dir| dir.name.as_str() == "link") .any(|link| { if let Some(url_in_link) = link - .argument_by_name("url") + .specified_argument_by_name("url") .and_then(|value| value.as_str()) { let Some((base_url_in_link, version_in_link)) = url_in_link.rsplit_once("/v") @@ -295,7 +297,7 @@ impl Schema { .filter(|dir| dir.name.as_str() == "link") .find(|link| { if let Some(url_in_link) = link - .argument_by_name("url") + .specified_argument_by_name("url") .and_then(|value| value.as_str()) { let Some((base_url_in_link, version_in_link)) = url_in_link.rsplit_once("/v") @@ -319,7 +321,7 @@ impl Schema { } }) .map(|link| { - link.argument_by_name("as") + link.specified_argument_by_name("as") .and_then(|value| value.as_str().map(|s| s.to_string())) .unwrap_or_else(|| default.to_string()) }) diff --git a/apollo-router/src/spec/selection.rs b/apollo-router/src/spec/selection.rs index 0c0cd545b8..f9ac7e42b9 100644 --- a/apollo-router/src/spec/selection.rs +++ b/apollo-router/src/spec/selection.rs @@ -304,7 +304,7 @@ fn parse_defer( } let label = if condition != Condition::No { directive - .argument_by_name("label") + .specified_argument_by_name("label") .and_then(|value| value.as_str()) .map(|str| str.to_owned()) } else { @@ -355,7 +355,7 @@ impl IncludeSkip { impl Condition { pub(crate) fn parse(directive: &executable::Directive) -> Option { - match directive.argument_by_name("if")?.as_ref() { + match directive.specified_argument_by_name("if")?.as_ref() { executable::Value::Boolean(true) => Some(Condition::Yes), executable::Value::Boolean(false) => Some(Condition::No), executable::Value::Variable(variable) => { diff --git a/apollo-router/src/uplink/license_enforcement.rs b/apollo-router/src/uplink/license_enforcement.rs index 1f20719436..1d23f9cc6c 100644 --- a/apollo-router/src/uplink/license_enforcement.rs +++ b/apollo-router/src/uplink/license_enforcement.rs @@ -104,7 +104,7 @@ impl ParsedLinkSpec { link_directive: &Directive, ) -> Option> { link_directive - .argument_by_name(LINK_URL_ARGUMENT) + .specified_argument_by_name(LINK_URL_ARGUMENT) .and_then(|value| { let url_string = value.as_str(); let parsed_url = Url::parse(url_string.unwrap_or_default()).ok()?; @@ -122,7 +122,7 @@ impl ParsedLinkSpec { semver::Version::parse(format!("{}.0", &version_string).as_str()).ok()?; let imported_as = link_directive - .argument_by_name(LINK_AS_ARGUMENT) + .specified_argument_by_name(LINK_AS_ARGUMENT) .map(|as_arg| as_arg.as_str().unwrap_or_default().to_string()); Some(Ok(ParsedLinkSpec { @@ -274,7 +274,9 @@ impl LicenseEnforcementReport { } _ => vec![], }) - .any(|directive| directive.argument_by_name(argument).is_some()) + .any(|directive| { + directive.specified_argument_by_name(argument).is_some() + }) { schema_violations.push(SchemaViolation::DirectiveArgument { url: link_spec.url.to_string(), diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index 664f8970a6..826a377e04 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -488,6 +488,11 @@ impl IntegrationTest { .expect("must be able to write config"); } + #[allow(dead_code)] + pub fn update_subgraph_overrides(&mut self, overrides: HashMap) { + self._subgraph_overrides = overrides; + } + #[allow(dead_code)] pub async fn update_schema(&self, supergraph_path: &PathBuf) { fs::copy(supergraph_path, &self.test_schema_location).expect("could not write schema"); diff --git a/apollo-router/tests/fixtures/introspect_full_schema.graphql b/apollo-router/tests/fixtures/introspect_full_schema.graphql index a0a3100c4f..85d1b5e72f 100644 --- a/apollo-router/tests/fixtures/introspect_full_schema.graphql +++ b/apollo-router/tests/fixtures/introspect_full_schema.graphql @@ -19,6 +19,9 @@ query IntrospectionQuery { args(includeDeprecated: true) { ...InputValue } + nonDeprecatedArgs: args { + name + } } } } @@ -32,15 +35,24 @@ fragment FullType on __Type { args(includeDeprecated: true) { ...InputValue } + nonDeprecatedArgs: args { + name + } type { ...TypeRef } isDeprecated deprecationReason } + nonDeprecatedFields: fields { + name + } inputFields(includeDeprecated: true) { ...InputValue } + nonDeprecatedInputFields: inputFields { + name + } interfaces { ...TypeRef } @@ -50,6 +62,9 @@ fragment FullType on __Type { isDeprecated deprecationReason } + nonDeprecatedEnumValues: enumValues { + name + } possibleTypes { ...TypeRef } diff --git a/apollo-router/tests/fixtures/schema_to_introspect.graphql b/apollo-router/tests/fixtures/schema_to_introspect.graphql new file mode 100644 index 0000000000..f45dfa4335 --- /dev/null +++ b/apollo-router/tests/fixtures/schema_to_introspect.graphql @@ -0,0 +1,89 @@ +"The schema" +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: TheQuery +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar join__FieldSet +scalar link__Import + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "subgraph1", url: "http://localhost:4001/graphql") +} + +enum link__Purpose { + SECURITY + EXECUTION +} + +""" +Root query type +""" +type TheQuery implements I @join__type(graph: SUBGRAPH1) { + id: ID! + ints: [[Int!]]! @deprecated(reason: "
") + url(arg: In = { b: 4, a: 2 }): Url + union: U @deprecated(reason: null) +} + +interface I @join__type(graph: SUBGRAPH1) { + id: ID! +} + +input In @join__type(graph: SUBGRAPH1) { + a: Int! = 0 @deprecated(reason: null) + b: Int @deprecated +} + +scalar Url @specifiedBy(url: "https://url.spec.whatwg.org/") @join__type(graph: SUBGRAPH1) + +union U @join__type(graph: SUBGRAPH1) = TheQuery | T + +type T @join__type(graph: SUBGRAPH1) { + enum: E @deprecated +} + +enum E @join__type(graph: SUBGRAPH1) { + NEW + OLD @deprecated +} diff --git a/apollo-router/tests/integration/introspection.rs b/apollo-router/tests/integration/introspection.rs index 64aea564d5..ac5ae33915 100644 --- a/apollo-router/tests/integration/introspection.rs +++ b/apollo-router/tests/integration/introspection.rs @@ -238,17 +238,40 @@ async fn both_mode_integration() { introspection: true ", ) - .supergraph("../examples/graphql/local.graphql") - .log("error,apollo_router=info,apollo_router::query_planner=debug") + .supergraph("tests/fixtures/schema_to_introspect.graphql") + .log("error,apollo_router=info,apollo_router::query_planner=trace") .build() .await; router.start().await; router.assert_started().await; - router - .execute_query(&json!({ - "query": include_str!("../fixtures/introspect_full_schema.graphql"), - })) - .await; + let query = json!({ + "query": include_str!("../fixtures/introspect_full_schema.graphql"), + }); + let (_trace_id, response) = router.execute_query(&query).await; + insta::assert_json_snapshot!(response.json::().await.unwrap()); router.assert_log_contains("Introspection match! 🎉").await; router.graceful_shutdown().await; } + +#[tokio::test] +async fn integration() { + let mut router = IntegrationTest::builder() + .config( + " + experimental_introspection_mode: new + supergraph: + introspection: true + ", + ) + .supergraph("tests/fixtures/schema_to_introspect.graphql") + .build() + .await; + router.start().await; + router.assert_started().await; + let query = json!({ + "query": include_str!("../fixtures/introspect_full_schema.graphql"), + }); + let (_trace_id, response) = router.execute_query(&query).await; + insta::assert_json_snapshot!(response.json::().await.unwrap()); + router.graceful_shutdown().await; +} diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index cbd1f5dc0a..bb8bb5c38e 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -48,7 +48,7 @@ use crate::integration::IntegrationTest; async fn query_planner_cache() -> Result<(), BoxError> { // If this test fails and the cache key format changed you'll need to update the key here. // Look at the top of the file for instructions on getting the new cache key. - let known_cache_key = "plan:0:v2.9.1:70f115ebba5991355c17f4f56ba25bb093c519c4db49a30f3b10de279a4e3fa4:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:4f9f0183101b2f249a364b98adadfda6e5e2001d1f2465c988428cf1ac0b545f"; + let known_cache_key = "plan:0:v2.9.2:70f115ebba5991355c17f4f56ba25bb093c519c4db49a30f3b10de279a4e3fa4:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:4f9f0183101b2f249a364b98adadfda6e5e2001d1f2465c988428cf1ac0b545f"; let config = RedisConfig::from_url("redis://127.0.0.1:6379").unwrap(); let client = RedisClient::new(config, None, None, None); @@ -944,7 +944,7 @@ async fn connection_failure_blocks_startup() { async fn query_planner_redis_update_query_fragments() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_query_fragments.router.yaml"), - "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:78f3ccab3def369f4b809a0f8c8f6e90545eb08cd1efeb188ffc663b902c1f2d", + "plan:0:v2.9.2:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:78f3ccab3def369f4b809a0f8c8f6e90545eb08cd1efeb188ffc663b902c1f2d", ) .await; } @@ -974,7 +974,7 @@ async fn query_planner_redis_update_introspection() { // test just passes locally. test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_introspection.router.yaml"), - "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:99a70d6c967eea3bc68721e1094f586f5ae53c7e12f83a650abd5758c372d048", + "plan:0:v2.9.2:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:99a70d6c967eea3bc68721e1094f586f5ae53c7e12f83a650abd5758c372d048", ) .await; } @@ -994,7 +994,7 @@ async fn query_planner_redis_update_defer() { // test just passes locally. test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), - "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:d6a3d7807bb94cfb26be4daeb35e974680b53755658fafd4c921c70cec1b7c39", + "plan:0:v2.9.2:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:d6a3d7807bb94cfb26be4daeb35e974680b53755658fafd4c921c70cec1b7c39", ) .await; } @@ -1016,7 +1016,7 @@ async fn query_planner_redis_update_type_conditional_fetching() { include_str!( "fixtures/query_planner_redis_config_update_type_conditional_fetching.router.yaml" ), - "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:8991411cc7b66d9f62ab1e661f2ce9ccaf53b0d203a275e43ced9a8b6bba02dd", + "plan:0:v2.9.2:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:8991411cc7b66d9f62ab1e661f2ce9ccaf53b0d203a275e43ced9a8b6bba02dd", ) .await; } @@ -1038,7 +1038,7 @@ async fn query_planner_redis_update_reuse_query_fragments() { include_str!( "fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml" ), - "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:c05e89caeb8efc4e8233e8648099b33414716fe901e714416fd0f65a67867f07", + "plan:0:v2.9.2:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:c05e89caeb8efc4e8233e8648099b33414716fe901e714416fd0f65a67867f07", ) .await; } @@ -1063,7 +1063,8 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key router.clear_redis_cache().await; // If the tests above are failing, this is the key that needs to be changed first. - let starting_key = "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:a52c81e3e2e47c8363fbcd2653e196431c15716acc51fce4f58d9368ac4c2d8d"; + let starting_key = "plan:0:v2.9.2:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:a52c81e3e2e47c8363fbcd2653e196431c15716acc51fce4f58d9368ac4c2d8d"; + assert_ne!(starting_key, new_cache_key, "starting_key (cache key for the initial config) and new_cache_key (cache key with the updated config) should not be equal. This either means that the cache key is not being generated correctly, or that the test is not actually checking the updated key."); router.execute_default_query().await; router.assert_redis_cache_contains(starting_key, None).await; diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__introspection__both_mode_integration.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__introspection__both_mode_integration.snap new file mode 100644 index 0000000000..1bad697545 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__introspection__both_mode_integration.snap @@ -0,0 +1,1728 @@ +--- +source: apollo-router/tests/integration/introspection.rs +expression: "response.json::().await.unwrap()" +--- +{ + "data": { + "__schema": { + "queryType": { + "name": "TheQuery" + }, + "mutationType": null, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "TheQuery", + "description": "Root query type", + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ints", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + } + } + } + }, + "isDeprecated": true, + "deprecationReason": "
" + }, + { + "name": "url", + "description": null, + "args": [ + { + "name": "arg", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "In", + "ofType": null + }, + "defaultValue": "{a: 2, b: 4}", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "arg" + } + ], + "type": { + "kind": "SCALAR", + "name": "Url", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "union", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "UNION", + "name": "U", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "id" + }, + { + "name": "url" + }, + { + "name": "union" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "I", + "ofType": null + } + ], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "I", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "id" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "TheQuery", + "ofType": null + } + ] + }, + { + "kind": "INPUT_OBJECT", + "name": "In", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": [ + { + "name": "a", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": "0", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "b", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": true, + "deprecationReason": "No longer supported" + } + ], + "nonDeprecatedInputFields": [ + { + "name": "a" + } + ], + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Url", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "U", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "TheQuery", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "T", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "T", + "description": null, + "fields": [ + { + "name": "enum", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "ENUM", + "name": "E", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "No longer supported" + } + ], + "nonDeprecatedFields": [], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "E", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "NEW", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OLD", + "description": null, + "isDeprecated": true, + "deprecationReason": "No longer supported" + } + ], + "nonDeprecatedEnumValues": [ + { + "name": "NEW" + } + ], + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "description" + }, + { + "name": "types" + }, + { + "name": "queryType" + }, + { + "name": "mutationType" + }, + { + "name": "subscriptionType" + }, + { + "name": "directives" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByURL`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "specifiedByURL", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "kind" + }, + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "specifiedByURL" + }, + { + "name": "fields" + }, + { + "name": "interfaces" + }, + { + "name": "possibleTypes" + }, + { + "name": "enumValues" + }, + { + "name": "inputFields" + }, + { + "name": "ofType" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given `__Type` is.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedEnumValues": [ + { + "name": "SCALAR" + }, + { + "name": "OBJECT" + }, + { + "name": "INTERFACE" + }, + { + "name": "UNION" + }, + { + "name": "ENUM" + }, + { + "name": "INPUT_OBJECT" + }, + { + "name": "LIST" + }, + { + "name": "NON_NULL" + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "args" + }, + { + "name": "type" + }, + { + "name": "isDeprecated" + }, + { + "name": "deprecationReason" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": "A GraphQL-formatted string representing the default value for this input value.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "type" + }, + { + "name": "defaultValue" + }, + { + "name": "isDeprecated" + }, + { + "name": "deprecationReason" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "isDeprecated" + }, + { + "name": "deprecationReason" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isRepeatable", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "isRepeatable" + }, + { + "name": "locations" + }, + { + "name": "args" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Location adjacent to a query operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Location adjacent to a mutation operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Location adjacent to a subscription operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Location adjacent to a field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Location adjacent to a fragment definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Location adjacent to a fragment spread.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Location adjacent to an inline fragment.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VARIABLE_DEFINITION", + "description": "Location adjacent to a variable definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Location adjacent to a schema definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Location adjacent to a scalar definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Location adjacent to an object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Location adjacent to a field definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Location adjacent to an argument definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Location adjacent to an interface definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Location adjacent to a union definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Location adjacent to an enum definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Location adjacent to an enum value definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Location adjacent to an input object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Location adjacent to an input object field definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedEnumValues": [ + { + "name": "QUERY" + }, + { + "name": "MUTATION" + }, + { + "name": "SUBSCRIPTION" + }, + { + "name": "FIELD" + }, + { + "name": "FRAGMENT_DEFINITION" + }, + { + "name": "FRAGMENT_SPREAD" + }, + { + "name": "INLINE_FRAGMENT" + }, + { + "name": "VARIABLE_DEFINITION" + }, + { + "name": "SCHEMA" + }, + { + "name": "SCALAR" + }, + { + "name": "OBJECT" + }, + { + "name": "FIELD_DEFINITION" + }, + { + "name": "ARGUMENT_DEFINITION" + }, + { + "name": "INTERFACE" + }, + { + "name": "UNION" + }, + { + "name": "ENUM" + }, + { + "name": "ENUM_VALUE" + }, + { + "name": "INPUT_OBJECT" + }, + { + "name": "INPUT_FIELD_DEFINITION" + } + ], + "possibleTypes": null + } + ], + "directives": [ + { + "name": "defer", + "description": null, + "locations": [ + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "label", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "if", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": "true", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "label" + }, + { + "name": "if" + } + ] + }, + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "if" + } + ] + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "if" + } + ] + }, + { + "name": "deprecated", + "description": "Marks an element of a GraphQL schema as no longer supported.", + "locations": [ + "FIELD_DEFINITION", + "ARGUMENT_DEFINITION", + "INPUT_FIELD_DEFINITION", + "ENUM_VALUE" + ], + "args": [ + { + "name": "reason", + "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"No longer supported\"", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "reason" + } + ] + }, + { + "name": "specifiedBy", + "description": "Exposes a URL that specifies the behavior of this scalar.", + "locations": [ + "SCALAR" + ], + "args": [ + { + "name": "url", + "description": "The URL that specifies the behavior of this scalar.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "url" + } + ] + } + ] + } + } +} diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__introspection__integration.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__introspection__integration.snap new file mode 100644 index 0000000000..40e9d17440 --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__introspection__integration.snap @@ -0,0 +1,1728 @@ +--- +source: apollo-router/tests/integration/introspection.rs +expression: "response.json::().await.unwrap()" +--- +{ + "data": { + "__schema": { + "queryType": { + "name": "TheQuery" + }, + "mutationType": null, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "description" + }, + { + "name": "types" + }, + { + "name": "queryType" + }, + { + "name": "mutationType" + }, + { + "name": "subscriptionType" + }, + { + "name": "directives" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByURL`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "specifiedByURL", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "kind" + }, + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "fields" + }, + { + "name": "interfaces" + }, + { + "name": "possibleTypes" + }, + { + "name": "enumValues" + }, + { + "name": "inputFields" + }, + { + "name": "ofType" + }, + { + "name": "specifiedByURL" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given `__Type` is.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedEnumValues": [ + { + "name": "SCALAR" + }, + { + "name": "OBJECT" + }, + { + "name": "INTERFACE" + }, + { + "name": "UNION" + }, + { + "name": "ENUM" + }, + { + "name": "INPUT_OBJECT" + }, + { + "name": "LIST" + }, + { + "name": "NON_NULL" + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "args" + }, + { + "name": "type" + }, + { + "name": "isDeprecated" + }, + { + "name": "deprecationReason" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": "A GraphQL-formatted string representing the default value for this input value.", + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "type" + }, + { + "name": "defaultValue" + }, + { + "name": "isDeprecated" + }, + { + "name": "deprecationReason" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "isDeprecated" + }, + { + "name": "deprecationReason" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "includeDeprecated" + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isRepeatable", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "name" + }, + { + "name": "description" + }, + { + "name": "locations" + }, + { + "name": "args" + }, + { + "name": "isRepeatable" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Location adjacent to a query operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Location adjacent to a mutation operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Location adjacent to a subscription operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Location adjacent to a field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Location adjacent to a fragment definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Location adjacent to a fragment spread.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Location adjacent to an inline fragment.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VARIABLE_DEFINITION", + "description": "Location adjacent to a variable definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Location adjacent to a schema definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Location adjacent to a scalar definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Location adjacent to an object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Location adjacent to a field definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Location adjacent to an argument definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Location adjacent to an interface definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Location adjacent to a union definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Location adjacent to an enum definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Location adjacent to an enum value definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Location adjacent to an input object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Location adjacent to an input object field definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedEnumValues": [ + { + "name": "QUERY" + }, + { + "name": "MUTATION" + }, + { + "name": "SUBSCRIPTION" + }, + { + "name": "FIELD" + }, + { + "name": "FRAGMENT_DEFINITION" + }, + { + "name": "FRAGMENT_SPREAD" + }, + { + "name": "INLINE_FRAGMENT" + }, + { + "name": "VARIABLE_DEFINITION" + }, + { + "name": "SCHEMA" + }, + { + "name": "SCALAR" + }, + { + "name": "OBJECT" + }, + { + "name": "FIELD_DEFINITION" + }, + { + "name": "ARGUMENT_DEFINITION" + }, + { + "name": "INTERFACE" + }, + { + "name": "UNION" + }, + { + "name": "ENUM" + }, + { + "name": "ENUM_VALUE" + }, + { + "name": "INPUT_OBJECT" + }, + { + "name": "INPUT_FIELD_DEFINITION" + } + ], + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TheQuery", + "description": "Root query type", + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ints", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + } + } + } + }, + "isDeprecated": true, + "deprecationReason": "
" + }, + { + "name": "url", + "description": null, + "args": [ + { + "name": "arg", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "In", + "ofType": null + }, + "defaultValue": "{b: 4, a: 2}", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "arg" + } + ], + "type": { + "kind": "SCALAR", + "name": "Url", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "union", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "UNION", + "name": "U", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "id" + }, + { + "name": "url" + }, + { + "name": "union" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "I", + "ofType": null + } + ], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "I", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedFields": [ + { + "name": "id" + } + ], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "TheQuery", + "ofType": null + } + ] + }, + { + "kind": "INPUT_OBJECT", + "name": "In", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": [ + { + "name": "a", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": "0", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "b", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": true, + "deprecationReason": "No longer supported" + } + ], + "nonDeprecatedInputFields": [ + { + "name": "a" + } + ], + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Url", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "U", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "TheQuery", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "T", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "T", + "description": null, + "fields": [ + { + "name": "enum", + "description": null, + "args": [], + "nonDeprecatedArgs": [], + "type": { + "kind": "ENUM", + "name": "E", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "No longer supported" + } + ], + "nonDeprecatedFields": [], + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": [], + "enumValues": null, + "nonDeprecatedEnumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "E", + "description": null, + "fields": null, + "nonDeprecatedFields": null, + "inputFields": null, + "nonDeprecatedInputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "NEW", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OLD", + "description": null, + "isDeprecated": true, + "deprecationReason": "No longer supported" + } + ], + "nonDeprecatedEnumValues": [ + { + "name": "NEW" + } + ], + "possibleTypes": null + } + ], + "directives": [ + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "if" + } + ] + }, + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "if" + } + ] + }, + { + "name": "deprecated", + "description": "Marks an element of a GraphQL schema as no longer supported.", + "locations": [ + "FIELD_DEFINITION", + "ARGUMENT_DEFINITION", + "INPUT_FIELD_DEFINITION", + "ENUM_VALUE" + ], + "args": [ + { + "name": "reason", + "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"No longer supported\"", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "reason" + } + ] + }, + { + "name": "specifiedBy", + "description": "Exposes a URL that specifies the behavior of this scalar.", + "locations": [ + "SCALAR" + ], + "args": [ + { + "name": "url", + "description": "The URL that specifies the behavior of this scalar.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "url" + } + ] + }, + { + "name": "defer", + "description": null, + "locations": [ + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "label", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "if", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": "true", + "isDeprecated": false, + "deprecationReason": null + } + ], + "nonDeprecatedArgs": [ + { + "name": "label" + }, + { + "name": "if" + } + ] + } + ] + } + } +} diff --git a/apollo-router/tests/integration/telemetry/fixtures/json.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/json.router.yaml index 2e8b047638..fa8fba775e 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/json.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/json.router.yaml @@ -68,6 +68,12 @@ telemetry: eq: - "log" - response_header: "x-log-request" + my.response_event.on_event: + message: "my response event message" + level: warn + on: event_response + attributes: + on_supergraph_response_event: on_supergraph_event subgraph: # Standard events request: info diff --git a/apollo-router/tests/integration/telemetry/fixtures/json.sampler_off.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/json.sampler_off.router.yaml index 1bbd2ac994..3190c14d34 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/json.sampler_off.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/json.sampler_off.router.yaml @@ -51,6 +51,12 @@ telemetry: eq: - "log" - request_header: "x-log-request" + my.response_event.on_event: + message: "my response event message" + level: warn + on: event_response + attributes: + on_supergraph_response_event: on_supergraph_event my.request.event: message: "my event message" level: info diff --git a/apollo-router/tests/integration/telemetry/fixtures/text.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/text.router.yaml index 66c7508443..ef009e55bd 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/text.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/text.router.yaml @@ -52,6 +52,12 @@ telemetry: eq: - "log" - request_header: "x-log-request" + my.response_event.on_event: + message: "my response event message" + level: warn + on: event_response + attributes: + on_supergraph_response_event: on_supergraph_event my.request.event: message: "my event message" level: info diff --git a/apollo-router/tests/integration/telemetry/logging.rs b/apollo-router/tests/integration/telemetry/logging.rs index a50f0fa20e..64b43fe032 100644 --- a/apollo-router/tests/integration/telemetry/logging.rs +++ b/apollo-router/tests/integration/telemetry/logging.rs @@ -30,6 +30,10 @@ async fn test_json() -> Result<(), BoxError> { router.execute_query(&query).await; router.assert_log_contains(r#""static_one":"test""#).await; router.execute_query(&query).await; + router + .assert_log_contains(r#""on_supergraph_response_event":"on_supergraph_event""#) + .await; + router.execute_query(&query).await; router.assert_log_contains(r#""response_status":200"#).await; router.graceful_shutdown().await; @@ -160,6 +164,10 @@ async fn test_json_sampler_off() -> Result<(), BoxError> { router.execute_query(&query).await; router.assert_log_contains(r#""static_one":"test""#).await; router.execute_query(&query).await; + router + .assert_log_contains(r#""on_supergraph_response_event":"on_supergraph_event""#) + .await; + router.execute_query(&query).await; router.assert_log_contains(r#""response_status":200"#).await; router.graceful_shutdown().await; @@ -188,6 +196,10 @@ async fn test_text() -> Result<(), BoxError> { router.assert_log_contains("trace_id").await; router.execute_query(&query).await; router.assert_log_contains("span_id").await; + router + .assert_log_contains(r#"on_supergraph_response_event=on_supergraph_event"#) + .await; + router.execute_query(&query).await; router.execute_query(&query).await; router.assert_log_contains("response_status=200").await; router.graceful_shutdown().await; diff --git a/apollo-router/tests/integration_tests.rs b/apollo-router/tests/integration_tests.rs index 4f99c0602d..378a20ddd5 100644 --- a/apollo-router/tests/integration_tests.rs +++ b/apollo-router/tests/integration_tests.rs @@ -1386,3 +1386,37 @@ async fn test_telemetry_doesnt_hang_with_invalid_schema() { ) .await; } + +// Ensure that, on unix, the router won't start with wrong file permissions +#[cfg(unix)] +#[test] +fn it_will_not_start_with_loose_file_permissions() { + use std::os::fd::AsRawFd; + use std::process::Command; + + use crate::integration::IntegrationTest; + + let mut router = Command::new(IntegrationTest::router_location()); + + let tester = tempfile::NamedTempFile::new().expect("it created a temporary test file"); + let fd = tester.as_file().as_raw_fd(); + let path = tester.path().to_str().expect("got the tempfile path"); + + // Modify our temporary file permissions so that they are definitely too loose. + unsafe { + libc::fchmod(fd, 0o777); + } + + let output = router + .args(["--apollo-key-path", path]) + .output() + .expect("router could not start"); + + // Assert that our router executed unsuccessfully + assert!(!output.status.success()); + // It may have been unsuccessful for a variety of reasons, is it the right reason? + assert_eq!( + std::str::from_utf8(&output.stderr).expect("output is a string"), + "Apollo key file permissions (0o777) are too permissive\n" + ) +} diff --git a/apollo-router/tests/samples_tests.rs b/apollo-router/tests/samples_tests.rs index e3fd0d5264..22e3b31e18 100644 --- a/apollo-router/tests/samples_tests.rs +++ b/apollo-router/tests/samples_tests.rs @@ -161,7 +161,13 @@ impl TestExecution { Action::ReloadSchema { schema_path } => { self.reload_schema(schema_path, path, out).await } - Action::ReloadSubgraphs { subgraphs } => self.reload_subgraphs(subgraphs, out).await, + Action::ReloadSubgraphs { + subgraphs, + update_url_overrides, + } => { + self.reload_subgraphs(subgraphs, *update_url_overrides, out) + .await + } Action::Request { request, query_path, @@ -193,50 +199,11 @@ impl TestExecution { path: &Path, out: &mut String, ) -> Result<(), Failed> { - let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap(); - let address = listener.local_addr().unwrap(); - let url = format!("http://{address}/"); - - let subgraphs_server = wiremock::MockServer::builder() - .listener(listener) - .start() - .await; - - writeln!(out, "subgraphs listening on {url}").unwrap(); - - let mut subgraph_overrides = HashMap::new(); - - for (name, subgraph) in subgraphs { - for SubgraphRequestMock { request, response } in &subgraph.requests { - let mut builder = Mock::given(body_partial_json(&request.body)); - - if let Some(s) = request.method.as_deref() { - builder = builder.and(method(s)); - } - - if let Some(s) = request.path.as_deref() { - builder = builder.and(wiremock::matchers::path(s)); - } - - for (header_name, header_value) in &request.headers { - builder = builder.and(header(header_name.as_str(), header_value.as_str())); - } - - let mut res = ResponseTemplate::new(response.status.unwrap_or(200)); - for (header_name, header_value) in &response.headers { - res = res.append_header(header_name.as_str(), header_value.as_str()); - } - builder - .respond_with(res.set_body_json(&response.body)) - .mount(&subgraphs_server) - .await; - } + self.subgraphs = subgraphs.clone(); + let (mut subgraphs_server, url) = self.start_subgraphs(out).await; - // Add a default override for products, if not specified - subgraph_overrides - .entry(name.to_string()) - .or_insert(url.clone()); - } + let subgraph_overrides = self.load_subgraph_mocks(&mut subgraphs_server, &url).await; + writeln!(out, "got subgraph mocks: {subgraph_overrides:?}").unwrap(); let config = open_file(&path.join(configuration_path), out)?; let schema_path = path.join(schema_path); @@ -253,7 +220,6 @@ impl TestExecution { self.router = Some(router); self.subgraphs_server = Some(subgraphs_server); - self.subgraphs = subgraphs.clone(); self.configuration_path = Some(configuration_path.to_string()); Ok(()) @@ -265,6 +231,21 @@ impl TestExecution { path: &Path, out: &mut String, ) -> Result<(), Failed> { + let mut subgraphs_server = match self.subgraphs_server.take() { + Some(subgraphs_server) => subgraphs_server, + None => self.start_subgraphs(out).await.0, + }; + subgraphs_server.reset().await; + + let subgraph_url = Self::subgraph_url(&subgraphs_server); + let subgraph_overrides = self + .load_subgraph_mocks(&mut subgraphs_server, &subgraph_url) + .await; + + let config = open_file(&path.join(configuration_path), out)?; + self.configuration_path = Some(configuration_path.to_string()); + self.subgraphs_server = Some(subgraphs_server); + let router = match self.router.as_mut() { None => { writeln!( @@ -277,6 +258,14 @@ impl TestExecution { Some(router) => router, }; + router.update_subgraph_overrides(subgraph_overrides); + router.update_config(&config).await; + router.assert_reloaded().await; + + Ok(()) + } + + async fn start_subgraphs(&mut self, out: &mut String) -> (MockServer, String) { let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap(); let address = listener.local_addr().unwrap(); let url = format!("http://{address}/"); @@ -288,6 +277,18 @@ impl TestExecution { writeln!(out, "subgraphs listening on {url}").unwrap(); + (subgraphs_server, url) + } + + fn subgraph_url(server: &MockServer) -> String { + format!("http://{}/", server.address()) + } + + async fn load_subgraph_mocks( + &mut self, + subgraphs_server: &mut MockServer, + url: &str, + ) -> HashMap { let mut subgraph_overrides = HashMap::new(); for (name, subgraph) in &self.subgraphs { @@ -312,61 +313,57 @@ impl TestExecution { } builder .respond_with(res.set_body_json(&response.body)) - .mount(&subgraphs_server) + .mount(subgraphs_server) .await; } // Add a default override for products, if not specified subgraph_overrides .entry(name.to_string()) - .or_insert(url.clone()); + .or_insert(url.to_owned()); } - let config = open_file(&path.join(configuration_path), out)?; - self.configuration_path = Some(configuration_path.to_string()); - self.subgraphs_server = Some(subgraphs_server); - - router.update_config(&config).await; - router.assert_reloaded().await; - - Ok(()) + subgraph_overrides } async fn reload_subgraphs( &mut self, subgraphs: &HashMap, + update_url_overrides: bool, out: &mut String, ) -> Result<(), Failed> { writeln!(out, "reloading subgraphs with: {subgraphs:?}").unwrap(); - let subgraphs_server = self.subgraphs_server.as_mut().unwrap(); + let mut subgraphs_server = match self.subgraphs_server.take() { + Some(subgraphs_server) => subgraphs_server, + None => self.start_subgraphs(out).await.0, + }; subgraphs_server.reset().await; - for subgraph in subgraphs.values() { - for SubgraphRequestMock { request, response } in &subgraph.requests { - let mut builder = Mock::given(body_partial_json(&request.body)); - - if let Some(s) = request.method.as_deref() { - builder = builder.and(method(s)); - } - - if let Some(s) = request.path.as_deref() { - builder = builder.and(wiremock::matchers::path(s)); - } + self.subgraphs = subgraphs.clone(); - for (header_name, header_value) in &request.headers { - builder = builder.and(header(header_name.as_str(), header_value.as_str())); - } + let subgraph_url = Self::subgraph_url(&subgraphs_server); + let subgraph_overrides = self + .load_subgraph_mocks(&mut subgraphs_server, &subgraph_url) + .await; + self.subgraphs_server = Some(subgraphs_server); - let mut res = ResponseTemplate::new(response.status.unwrap_or(200)); - for (header_name, header_value) in &response.headers { - res = res.append_header(header_name.as_str(), header_value.as_str()); - } - builder - .respond_with(res.set_body_json(&response.body)) - .mount(subgraphs_server) - .await; + let router = match self.router.as_mut() { + None => { + writeln!( + out, + "cannot reload subgraph overrides: router was not started" + ) + .unwrap(); + return Err(out.into()); } + Some(router) => router, + }; + + if update_url_overrides { + router.update_subgraph_overrides(subgraph_overrides); + router.touch_config().await; + router.assert_reloaded().await; } Ok(()) @@ -576,6 +573,9 @@ enum Action { }, ReloadSubgraphs { subgraphs: HashMap, + // set to true if subgraph URL overrides should be updated (ex: a new subgraph is added) + #[serde(default)] + update_url_overrides: bool, }, Request { request: Value, diff --git a/dockerfiles/tracing/docker-compose.datadog.yml b/dockerfiles/tracing/docker-compose.datadog.yml index 3b58c1d702..4b3558d572 100644 --- a/dockerfiles/tracing/docker-compose.datadog.yml +++ b/dockerfiles/tracing/docker-compose.datadog.yml @@ -3,7 +3,7 @@ services: apollo-router: container_name: apollo-router - image: ghcr.io/apollographql/router:v1.55.0 + image: ghcr.io/apollographql/router:v1.56.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/datadog.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.jaeger.yml b/dockerfiles/tracing/docker-compose.jaeger.yml index fcb75930d2..ca7b4ab265 100644 --- a/dockerfiles/tracing/docker-compose.jaeger.yml +++ b/dockerfiles/tracing/docker-compose.jaeger.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router #build: ./router - image: ghcr.io/apollographql/router:v1.55.0 + image: ghcr.io/apollographql/router:v1.56.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/jaeger.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml index 0cb933a7ac..564933ab0c 100644 --- a/dockerfiles/tracing/docker-compose.zipkin.yml +++ b/dockerfiles/tracing/docker-compose.zipkin.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router build: ./router - image: ghcr.io/apollographql/router:v1.55.0 + image: ghcr.io/apollographql/router:v1.56.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/zipkin.router.yaml:/etc/config/configuration.yaml diff --git a/docs/source/configuration/overview.mdx b/docs/source/configuration/overview.mdx index 13e6991a6a..0caa139fe9 100644 --- a/docs/source/configuration/overview.mdx +++ b/docs/source/configuration/overview.mdx @@ -49,11 +49,26 @@ The graph ref for the GraphOS graph and variant that the router fetches its supe The [graph API key](/graphos/api-keys/#graph-api-keys) that the router should use to authenticate with GraphOS when fetching its supergraph schema. -**Required** when using [managed federation](/federation/managed-federation/overview/), except when using an [offline license](#--license) to run the router. +**Required** when using [managed federation](/federation/managed-federation/overview/), except when using an [offline license](#--license) to run the router or when using `APOLLO_KEY_PATH`. + + + +##### `APOLLO_KEY_PATH` + + + +⚠ **This is not available on Windows.** + +A path to a file containing the [graph API key](/graphos/api-keys/#graph-api-keys) that the router should use to authenticate with GraphOS when fetching its supergraph schema. + +**Required** when using [managed federation](/federation/managed-federation/overview/), except when using an [offline license](#--license) to run the router or when using `APOLLO_KEY`. + + + @@ -114,6 +129,23 @@ To learn how to compose your supergraph schema with the Rover CLI, see the [Fede The absolute or relative path to the router's optional [YAML configuration file](#yaml-config-file). + + + + + + +##### `--apollo-key-path` + +`APOLLO_KEY_PATH` + + + + +⚠ **This is not available on Windows.** + +The absolute or relative path to a file containing the Apollo graph API key for use with managed federation. + diff --git a/docs/source/federation-version-support.mdx b/docs/source/federation-version-support.mdx index 80ff912745..defc58f1c4 100644 --- a/docs/source/federation-version-support.mdx +++ b/docs/source/federation-version-support.mdx @@ -37,7 +37,15 @@ The table below shows which version of federation each router release is compile - v1.55.0 and later (see latest releases) + v1.56.0 and later (see latest releases) + + + 2.9.2 + + + + + v1.55.0 2.9.1 diff --git a/examples/supergraph-sdl/rust/Cargo.toml b/examples/supergraph-sdl/rust/Cargo.toml index ead321ca05..827e44ed5d 100644 --- a/examples/supergraph-sdl/rust/Cargo.toml +++ b/examples/supergraph-sdl/rust/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] anyhow = "1" -apollo-compiler = "=1.0.0-beta.23" +apollo-compiler = "=1.0.0-beta.24" apollo-router = { path = "../../../apollo-router" } async-trait = "0.1" tower = { version = "0.4", features = ["full"] } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 38603f704b..797f895ed3 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -20,7 +20,7 @@ reqwest = { workspace = true, features = ["json", "blocking"] } serde_json.workspace = true tokio.workspace = true # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.6.2+v2.9.1" +router-bridge = "=0.6.3+v2.9.2" [dev-dependencies] anyhow = "1" diff --git a/helm/chart/router/Chart.yaml b/helm/chart/router/Chart.yaml index 7a1c6d615a..9cbe1d5e12 100644 --- a/helm/chart/router/Chart.yaml +++ b/helm/chart/router/Chart.yaml @@ -20,10 +20,10 @@ type: application # so it matches the shape of our release process and release automation. # By proxy of that decision, this version uses SemVer 2.0.0, though the prefix # of "v" is not included. -version: 1.55.0 +version: 1.56.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.55.0" +appVersion: "v1.56.0" diff --git a/helm/chart/router/README.md b/helm/chart/router/README.md index fb09765b10..181bb9602c 100644 --- a/helm/chart/router/README.md +++ b/helm/chart/router/README.md @@ -2,7 +2,7 @@ [router](https://github.com/apollographql/router) Rust Graph Routing runtime for Apollo Federation -![Version: 1.55.0](https://img.shields.io/badge/Version-1.55.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.55.0](https://img.shields.io/badge/AppVersion-v1.55.0-informational?style=flat-square) +![Version: 1.56.0](https://img.shields.io/badge/Version-1.56.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.56.0](https://img.shields.io/badge/AppVersion-v1.56.0-informational?style=flat-square) ## Prerequisites @@ -11,7 +11,7 @@ ## Get Repo Info ```console -helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0 +helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.56.0 ``` ## Install Chart @@ -19,7 +19,7 @@ helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0 **Important:** only helm3 is supported ```console -helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0 --values my-values.yaml +helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.56.0 --values my-values.yaml ``` _See [configuration](#configuration) below._ diff --git a/licenses.html b/licenses.html index e415d536c4..764c755e37 100644 --- a/licenses.html +++ b/licenses.html @@ -44,7 +44,7 @@

Third Party Licenses

Overview of licenses:

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

    Used by:

    Apache License 2.0

    Used by:

    diff --git a/scripts/install.sh b/scripts/install.sh index f03b383d8d..1346bb9b5e 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -11,7 +11,7 @@ BINARY_DOWNLOAD_PREFIX="https://github.com/apollographql/router/releases/downloa # Router version defined in apollo-router's Cargo.toml # Note: Change this line manually during the release steps. -PACKAGE_VERSION="v1.55.0" +PACKAGE_VERSION="v1.56.0" download_binary() { downloader --check