From f41065e726b30b52535f7211b5f5ff9db5f4ef20 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 16 May 2024 13:21:23 +0300 Subject: [PATCH 01/10] chore(docs): Update RELEASE_CHECKLIST.md (#5179) Co-authored-by: Simon Sapin --- Cargo.lock | 2 +- RELEASE_CHECKLIST.md | 49 ++++++++++++++++++++++-------------- apollo-federation/Cargo.toml | 2 +- apollo-federation/README.md | 10 ++++++++ apollo-router/Cargo.toml | 2 +- 5 files changed, 43 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8826e21ce..7d86622644 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,7 +220,7 @@ dependencies = [ [[package]] name = "apollo-federation" -version = "0.0.11" +version = "1.46.0" dependencies = [ "apollo-compiler", "derive_more", diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md index c69c852b47..f6f2638694 100644 --- a/RELEASE_CHECKLIST.md +++ b/RELEASE_CHECKLIST.md @@ -121,7 +121,7 @@ Start following the steps below to start a release PR. The process is **not ful 8. Now, open a draft PR with a small boilerplate header from the branch which was just pushed: ``` - cat < **Note** > **This particular PR must be true-merged to \`main\`.** @@ -176,25 +176,29 @@ Start following the steps below to start a release PR. The process is **not ful - Run our compliance checks and update the `licenses.html` file as appropriate. - Ensure we're not using any incompatible licenses in the release. -7. Now, review and stage he changes produced by the previous step. This is most safely done using the `--patch` (or `-p`) flag to `git add` (`-u` ignores untracked files). +7. Until the script above is updated to do automate this step, + increment the version number in `apollo-federation/Cargo.toml` to match + that of `apollo-router/Cargo.toml`. + +8. Now, review and stage he changes produced by the previous step. This is most safely done using the `--patch` (or `-p`) flag to `git add` (`-u` ignores untracked files). ``` git add -up . ``` -8. Now commit those changes locally, using a brief message: +9. Now commit those changes locally, using a brief message: ``` git commit -m "prep release: v${APOLLO_ROUTER_RELEASE_VERSION}${APOLLO_ROUTER_PRERELEASE_SUFFIX}" ``` -9. Push this commit up to the existing release PR: +10. Push this commit up to the existing release PR: ``` git push "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "${APOLLO_ROUTER_RELEASE_VERSION}" ``` -10. Git tag the current commit and & push the branch and the pre-release tag simultaneously: +11. Git tag the current commit and & push the branch and the pre-release tag simultaneously: This process will kick off the bulk of the release process on CircleCI, including building each architecture on its own infrastructure and notarizing the macOS binary. @@ -203,11 +207,12 @@ Start following the steps below to start a release PR. The process is **not ful git push "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "${APOLLO_ROUTER_RELEASE_VERSION}" "v${APOLLO_ROUTER_RELEASE_VERSION}${APOLLO_ROUTER_PRERELEASE_SUFFIX}" ``` -11. Finally, publish the Crate from your local computer (this also needs to be moved to CI, but requires changing the release containers to be Rust-enabled and to restore the caches): +12. Finally, publish the Crates from your local computer (this also needs to be moved to CI, but requires changing the release containers to be Rust-enabled and to restore the caches): > Note: This command may appear unnecessarily specific, but it will help avoid publishing a version to Crates.io that doesn't match what you're currently releasing. (e.g., in the event that you've changed branches in another window) ``` + cargo publish -p apollo-federation@"${APOLLO_ROUTER_RELEASE_VERSION}${APOLLO_ROUTER_PRERELEASE_SUFFIX}" cargo publish -p apollo-router@"${APOLLO_ROUTER_RELEASE_VERSION}${APOLLO_ROUTER_PRERELEASE_SUFFIX}" ``` @@ -364,15 +369,21 @@ Start following the steps below to start a release PR. The process is **not ful git pull "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "${APOLLO_ROUTER_RELEASE_VERSION}" ``` -6. Use the `gh` CLI to enable **auto-merge** (**_NOT_** auto-**_squash_**): +6. Mark the release PR as **Ready for Review** (it was previously opened as a draft!) + + ``` + gh --repo "${APOLLO_ROUTER_RELEASE_GITHUB_REPO}" pr ready "${APOLLO_ROUTER_RELEASE_VERSION}" + ``` + +7. Use the `gh` CLI to enable **auto-merge** (**_NOT_** auto-**_squash_**): ``` gh --repo "${APOLLO_ROUTER_RELEASE_GITHUB_REPO}" pr merge --merge --body "" -t "release: v${APOLLO_ROUTER_RELEASE_VERSION}" --auto "${APOLLO_ROUTER_RELEASE_VERSION}" ``` -7. 🗣️ **Solicit approval from the Router team, wait for the PR to pass CI and auto-merge into `main`** +8. 🗣️ **Solicit approval from the Router team, wait for the PR to pass CI and auto-merge into `main`** -8. After the PR has merged to `main`, pull `main` to your local terminal, and Git tag & push the release: +9. After the PR has merged to `main`, pull `main` to your local terminal, and Git tag & push the release: This process will kick off the bulk of the release process on CircleCI, including building each architecture on its own infrastructure and notarizing the macOS binary. @@ -383,13 +394,13 @@ Start following the steps below to start a release PR. The process is **not ful git push "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "v${APOLLO_ROUTER_RELEASE_VERSION}" ``` -9. Open a PR that reconciles `dev` (Make sure to merge this reconciliation PR back to dev, **do not squash or rebase**): +10. Open a PR that reconciles `dev` (Make sure to merge this reconciliation PR back to dev, **do not squash or rebase**): ``` gh --repo "${APOLLO_ROUTER_RELEASE_GITHUB_REPO}" pr create --title "Reconcile \`dev\` after merge to \`main\` for v${APOLLO_ROUTER_RELEASE_VERSION}" -B dev -H main --body "Follow-up to the v${APOLLO_ROUTER_RELEASE_VERSION} being officially released, bringing version bumps and changelog updates into the \`dev\` branch." ``` -10. Mark the PR to **auto-merge NOT auto-squash** using the URL that is output from the previous command +11. Mark the PR to **auto-merge NOT auto-squash** using the URL that is output from the previous command ``` APOLLO_RECONCILE_PR_URL=$(gh --repo "${APOLLO_ROUTER_RELEASE_GITHUB_REPO}" pr list --state open --base dev --head main --json url --jq '.[-1] | .url') @@ -398,15 +409,15 @@ Start following the steps below to start a release PR. The process is **not ful ``` -11. 🗣️ **Solicit approval from the Router team, wait for the PR to pass CI and auto-merge into `dev`** +12. 🗣️ **Solicit approval from the Router team, wait for the PR to pass CI and auto-merge into `dev`** -12. 👀 Follow along with the process by [going to CircleCI for the repository](https://app.circleci.com/pipelines/github/apollographql/router) and clicking on `release` for the Git tag that appears at the top of the list. +13. 👀 Follow along with the process by [going to CircleCI for the repository](https://app.circleci.com/pipelines/github/apollographql/router) and clicking on `release` for the Git tag that appears at the top of the list. -13. ⚠️ **Wait for `publish_github_release` on CircleCI to finish on this job before continuing.** ⚠️ +14. ⚠️ **Wait for `publish_github_release` on CircleCI to finish on this job before continuing.** ⚠️ You should expect this will take at least 30 minutes. -14. Re-create the file you may have previously created called `this_release.md` just to make sure its up to date after final edits from review: +15. Re-create the file you may have previously created called `this_release.md` _just to make sure_ it is up to date after final edits from review: ``` perl -0777 \ @@ -427,19 +438,19 @@ Start following the steps below to start a release PR. The process is **not ful CHANGELOG.md > this_release.md ``` -15. Change the links in `this_release.md` from `[@username](https://github.com/username)` to `@username` in order to facilitate the correct "Contributorship" attribution on the final GitHub release. +16. Change the links in `this_release.md` from `[@username](https://github.com/username)` to `@username` in order to facilitate the correct "Contributorship" attribution on the final GitHub release. ``` perl -pi -e 's/\[@([^\]]+)\]\([^)]+\)/@\1/g' this_release.md ``` -16. Update the release notes on the now-published [GitHub Releases](https://github.com/apollographql/router/releases) (this needs to be moved to CI, but requires `this_release.md` which we just created): +17. Update the release notes on the now-published [GitHub Releases](https://github.com/apollographql/router/releases) (this needs to be moved to CI, but requires `this_release.md` which we just created): ``` gh --repo "${APOLLO_ROUTER_RELEASE_GITHUB_REPO}" release edit v"${APOLLO_ROUTER_RELEASE_VERSION}" -F ./this_release.md ``` -17. Finally, publish the Crate from your local computer from the `main` branch (this also needs to be moved to CI, but requires changing the release containers to be Rust-enabled and to restore the caches): +18. Finally, publish the Crate from your local computer from the `main` branch (this also needs to be moved to CI, but requires changing the release containers to be Rust-enabled and to restore the caches): > Note: This command may appear unnecessarily specific, but it will help avoid publishing a version to Crates.io that doesn't match what you're currently releasing. (e.g., in the event that you've changed branches in another window) @@ -447,7 +458,7 @@ Start following the steps below to start a release PR. The process is **not ful cargo publish -p apollo-router@"${APOLLO_ROUTER_RELEASE_VERSION}" ``` -18. (Optional) To have a "social banner" for this release, run [this `htmlq` command](https://crates.io/crates/htmlq) (`cargo install htmlq`, or on MacOS `brew install htmlq`; its `jq` for HTML), open the link it produces, copy the image to your clipboard: +19. (Optional) To have a "social banner" for this release, run [this `htmlq` command](https://crates.io/crates/htmlq) (`cargo install htmlq`, or on MacOS `brew install htmlq`; its `jq` for HTML), open the link it produces, copy the image to your clipboard: ``` curl -s "https://github.com/apollographql/router/releases/tag/v${APOLLO_ROUTER_RELEASE_VERSION}" | htmlq 'meta[property="og:image"]' --attribute content diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml index 02b48dbfcd..570ceda815 100644 --- a/apollo-federation/Cargo.toml +++ b/apollo-federation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-federation" -version = "0.0.11" +version = "1.46.0" authors = ["The Apollo GraphQL Contributors"] edition = "2021" description = "Apollo Federation" diff --git a/apollo-federation/README.md b/apollo-federation/README.md index 0a506dcbe5..1dce0f1b1c 100644 --- a/apollo-federation/README.md +++ b/apollo-federation/README.md @@ -13,6 +13,16 @@ Federation 2 is an evolution of the original Apollo Federation with an improved Checkout the [Federation 2 docs](https://www.apollographql.com/docs/federation) and [demo repo](https://github.com/apollographql/supergraph-demo-fed2) to take it for a spin and [let us know what you think](https://community.apollographql.com/t/announcing-apollo-federation-2/1821)! +## Versioning + +The `apollo-federation` crate does **not** adhere to [Semantic Versioning](https://semver.org/). +Any version may have breaking API changes, as this API is expected to only be used by `apollo-router`. +Instead, the version number matches exactly that of the `apollo-router` crate version using it. + +This version number is **not** that of the Apollo Federation specification being implemented. +See [Router documentation](https://www.apollographql.com/docs/router/federation-version-support/) +for which Federation versions are supported by which Router versions. + ## Usage TODO diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 7a5c4590f9..59a9f73371 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -68,7 +68,7 @@ askama = "0.12.1" access-json = "0.1.0" anyhow = "1.0.80" apollo-compiler.workspace = true -apollo-federation = { path = "../apollo-federation", version = "=0.0.11" } +apollo-federation = { path = "../apollo-federation", version = "=1.46.0" } arc-swap = "1.6.0" async-channel = "1.9.0" async-compression = { version = "0.4.6", features = [ From 10a76bdf5c8938ead609784295119bdb004fc9fd Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Thu, 16 May 2024 19:20:07 +0200 Subject: [PATCH 02/10] file based integration tests (#5067) This introduces a new way to write integration tests, by using JSON files to describe the test plan. This has the benefit of not requiring any recompilation, at the cost of a slightly higher test time becomes it needs to start an entire router --- Cargo.lock | 26 +- apollo-router/Cargo.toml | 6 + apollo-router/tests/common.rs | 14 +- apollo-router/tests/samples/README.md | 119 +++++ .../tests/samples/basic/query1/README.md | 3 + .../samples/basic/query1/configuration.yaml | 4 + .../tests/samples/basic/query1/plan.json | 52 +++ .../samples/basic/query1/supergraph.graphql | 90 ++++ .../tests/samples/basic/query2/README.md | 3 + .../samples/basic/query2/configuration.yaml | 4 + .../tests/samples/basic/query2/plan.json | 26 ++ .../samples/basic/query2/supergraph.graphql | 90 ++++ apollo-router/tests/samples_tests.rs | 429 ++++++++++++++++++ 13 files changed, 861 insertions(+), 5 deletions(-) create mode 100644 apollo-router/tests/samples/README.md create mode 100644 apollo-router/tests/samples/basic/query1/README.md create mode 100644 apollo-router/tests/samples/basic/query1/configuration.yaml create mode 100644 apollo-router/tests/samples/basic/query1/plan.json create mode 100644 apollo-router/tests/samples/basic/query1/supergraph.graphql create mode 100644 apollo-router/tests/samples/basic/query2/README.md create mode 100644 apollo-router/tests/samples/basic/query2/configuration.yaml create mode 100644 apollo-router/tests/samples/basic/query2/plan.json create mode 100644 apollo-router/tests/samples/basic/query2/supergraph.graphql create mode 100644 apollo-router/tests/samples_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 7d86622644..25c47adfdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -324,6 +324,7 @@ dependencies = [ "jsonwebtoken", "lazy_static", "libc", + "libtest-mimic", "linkme", "lru", "maplit", @@ -2515,6 +2516,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "escape8259" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4911e3666fcd7826997b4745c8224295a6f3072f1418c3067b97a67557ee" +dependencies = [ + "rustversion", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -3900,6 +3910,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libtest-mimic" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fefdf21230d6143476a28adbee3d930e2b68a3d56443c777cae3fe9340eebff9" +dependencies = [ + "clap", + "escape8259", + "termcolor", + "threadpool", +] + [[package]] name = "libz-ng-sys" version = "1.1.12" @@ -7729,9 +7751,9 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 59a9f73371..5283633201 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -334,6 +334,7 @@ tracing-opentelemetry = "0.21.0" tracing-test = "0.2.4" walkdir = "2.4.0" wiremock = "0.5.22" +libtest-mimic = "0.7.2" [target.'cfg(target_os = "linux")'.dev-dependencies] rstack = { version = "0.3.3", features = ["dw"], default-features = false } @@ -353,6 +354,11 @@ serde_json.workspace = true name = "integration_tests" path = "tests/integration_tests.rs" +[[test]] +name = "samples" +path = "tests/samples_tests.rs" +harness = false + [[bench]] name = "huge_requests" harness = false diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index b10e484b94..c2ad6bca3d 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -73,11 +73,11 @@ use wiremock::ResponseTemplate; pub struct IntegrationTest { router: Option, test_config_location: PathBuf, + test_schema_location: PathBuf, router_location: PathBuf, stdio_tx: tokio::sync::mpsc::Sender, stdio_rx: tokio::sync::mpsc::Receiver, collect_stdio: Option<(tokio::sync::oneshot::Sender, regex::Regex)>, - supergraph: PathBuf, _subgraphs: wiremock::MockServer, telemetry: Telemetry, @@ -317,10 +317,13 @@ impl IntegrationTest { .await; let mut test_config_location = std::env::temp_dir(); + let mut test_schema_location = test_config_location.clone(); let location = format!("apollo-router-test-{}.yaml", Uuid::new_v4()); test_config_location.push(location); + test_schema_location.push(format!("apollo-router-test-{}.graphql", Uuid::new_v4())); fs::write(&test_config_location, &config_str).expect("could not write config"); + fs::copy(&supergraph, &test_schema_location).expect("could not write schema"); let (stdio_tx, stdio_rx) = tokio::sync::mpsc::channel(2000); let collect_stdio = collect_stdio.map(|sender| { @@ -332,10 +335,10 @@ impl IntegrationTest { router: None, router_location: Self::router_location(), test_config_location, + test_schema_location, stdio_tx, stdio_rx, collect_stdio, - supergraph, _subgraphs: subgraphs, _subgraph_overrides: subgraph_overrides, bind_address: Default::default(), @@ -383,7 +386,7 @@ impl IntegrationTest { "--config", &self.test_config_location.to_string_lossy(), "--supergraph", - &self.supergraph.to_string_lossy(), + &self.test_schema_location.to_string_lossy(), "--log", "error,apollo_router=info", ]) @@ -477,6 +480,11 @@ impl IntegrationTest { .expect("must be able to write config"); } + #[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"); + } + #[allow(dead_code)] pub fn execute_default_query( &self, diff --git a/apollo-router/tests/samples/README.md b/apollo-router/tests/samples/README.md new file mode 100644 index 0000000000..3c2d9122c9 --- /dev/null +++ b/apollo-router/tests/samples/README.md @@ -0,0 +1,119 @@ +# File based integration tests + +This folder contains a serie of Router integration tests that can be defined entirely through a JSON file. Thos tests are able to start and stop a router, reload its schema or configiration, make requests and check the expected response. While we can make similar tests from inside the Router's code, these tests here are faster to write and modify because they do not require recompilations of the Router, at the cost of a slightly higher runtime cost. + +## How to write a test + +One test is recognized as a folder containing a `plan.json` file. Any number of subfolders is accepted, and the test name will be the path to the test folder. If the folder contains a `README.md` file, it will be added to the captured output of the test, and displayed if the test failed. + +The `plan.json` file contains a top level JSON object with an `actions` field, containing an array of possible actions, that will be executed one by one: + +```json +{ + "actions": [ + { + "type": "Start", + "schema_path": "./supergraph.graphql", + "configuration_path": "./configuration.yaml", + "subgraphs": {} + }, + { + "type": "Request", + "request": { + "query": "{ me { name } }" + }, + "expected_response": { + "data":{ + "me":{ + "name":"Ada Lovelace" + } + } + } + }, + { + "type": "Stop" + } + ] +} +``` + +If any of those actions fails, the test will stop immediately. + +## Possible actions + +### Start + +```json +{ + "type": "Start", + "schema_path": "./supergraph.graphql", + "configuration_path": "./configuration.yaml", + "subgraphs": { + "accounts": { + "requests": [ + { + "request": {"query":"{me{name}}"}, + "response": {"data": { "me": { "name": "test" } } } + }, + { + "request": {"query":"{me{nom:name}}"}, + "response": {"data": { "me": { "nom": "test" } } } + } + ] + } + } +} +``` + +the `schema_path` and `configuration_path` field are relative to the test's folder. The `subgraph` field can contain mocked requests and responses for each subgraph. If the Router fails to load with this schema and configuration, then this action will fail the test. + +## Reload configuration + +Reloads the router with a new configuration file. If the Router fails to load the new configuration, then this action will fail the test. + +```json +{ + "type": "ReloadConfiguration", + "configuration_path": "./configuration.yaml" +} +``` + +## Reload schema + +Reloads the router with a new schema file. If the Router fails to load the new configuration, then this action will fail the test. + +```json +{ + "type": "ReloadSchema", + "schema_path": "./supergraph.graphql" +} +``` + +## Request + +Sends a request to the Router, and verifies that the response body matches the expected response. If it does not match or returned any HTTP error, then this action will fail the test. +```json +{ + "type": "Request", + "request": { + "query": "{ me { name } }" + }, + "expected_response": { + "data":{ + "me":{ + "name":"Ada Lovelace" + } + } + } +} +``` + +### Stop + +Stops the Router. If the Router does not stop correctly, then this action will fail the test. + +```json +{ + "type": "Stop" +} +``` \ No newline at end of file diff --git a/apollo-router/tests/samples/basic/query1/README.md b/apollo-router/tests/samples/basic/query1/README.md new file mode 100644 index 0000000000..9386489fb0 --- /dev/null +++ b/apollo-router/tests/samples/basic/query1/README.md @@ -0,0 +1,3 @@ +This is an example test + +This file adds some context that will be displayed on test failure \ No newline at end of file diff --git a/apollo-router/tests/samples/basic/query1/configuration.yaml b/apollo-router/tests/samples/basic/query1/configuration.yaml new file mode 100644 index 0000000000..f7ed04641e --- /dev/null +++ b/apollo-router/tests/samples/basic/query1/configuration.yaml @@ -0,0 +1,4 @@ +override_subgraph_url: + products: http://localhost:4005 +include_subgraph_errors: + all: true diff --git a/apollo-router/tests/samples/basic/query1/plan.json b/apollo-router/tests/samples/basic/query1/plan.json new file mode 100644 index 0000000000..7153b2dbf6 --- /dev/null +++ b/apollo-router/tests/samples/basic/query1/plan.json @@ -0,0 +1,52 @@ +{ + "actions": [ + { + "type": "Start", + "schema_path": "./supergraph.graphql", + "configuration_path": "./configuration.yaml", + "subgraphs": { + "accounts": { + "requests": [ + { + "request": {"query":"{me{name}}"}, + "response": {"data": { "me": { "name": "test" } } } + }, + { + "request": {"query":"{me{nom:name}}"}, + "response": {"data": { "me": { "nom": "test" } } } + } + ] + } + } + }, + { + "type": "Request", + "request": { + "query": "{ me { name } }" + }, + "expected_response": { + "data":{ + "me":{ + "name":"test" + } + } + } + }, + { + "type": "Request", + "request": { + "query": "{ me { nom: name } }" + }, + "expected_response": { + "data": { + "me": { + "nom": "test" + } + } + } + }, + { + "type": "Stop" + } + ] +} \ No newline at end of file diff --git a/apollo-router/tests/samples/basic/query1/supergraph.graphql b/apollo-router/tests/samples/basic/query1/supergraph.graphql new file mode 100644 index 0000000000..971174056a --- /dev/null +++ b/apollo-router/tests/samples/basic/query1/supergraph.graphql @@ -0,0 +1,90 @@ + +schema + @core(feature: "https://specs.apollo.dev/core/v0.2"), + @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) + @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) +{ + query: Query + mutation: Mutation +} + +directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION + +directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE + +directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION + +directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + +enum core__Purpose { + """ + `EXECUTION` features provide metadata necessary to for operation execution. + """ + EXECUTION + + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY +} + +scalar join__FieldSet + +enum join__Graph { + ACCOUNTS @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") + INVENTORY @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") + PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") + REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") +} +type Mutation { + createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) + createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +} + +type Product + @join__owner(graph: PRODUCTS) + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: REVIEWS, key: "upc") +{ + inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + name: String @join__field(graph: PRODUCTS) + price: Int @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) + reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + upc: String! @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: PRODUCTS) +} + +type Query { + me: User @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) +} + +type Review + @join__owner(graph: REVIEWS) + @join__type(graph: REVIEWS, key: "id") +{ + author: User @join__field(graph: REVIEWS, provides: "username") + body: String @join__field(graph: REVIEWS) + id: ID! @join__field(graph: REVIEWS) + product: Product @join__field(graph: REVIEWS) +} + +type User + @join__owner(graph: ACCOUNTS) + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") +{ + id: ID! @join__field(graph: ACCOUNTS) + name: String @join__field(graph: ACCOUNTS) + reviews: [Review] @join__field(graph: REVIEWS) + username: String @join__field(graph: ACCOUNTS) +} diff --git a/apollo-router/tests/samples/basic/query2/README.md b/apollo-router/tests/samples/basic/query2/README.md new file mode 100644 index 0000000000..9386489fb0 --- /dev/null +++ b/apollo-router/tests/samples/basic/query2/README.md @@ -0,0 +1,3 @@ +This is an example test + +This file adds some context that will be displayed on test failure \ No newline at end of file diff --git a/apollo-router/tests/samples/basic/query2/configuration.yaml b/apollo-router/tests/samples/basic/query2/configuration.yaml new file mode 100644 index 0000000000..f7ed04641e --- /dev/null +++ b/apollo-router/tests/samples/basic/query2/configuration.yaml @@ -0,0 +1,4 @@ +override_subgraph_url: + products: http://localhost:4005 +include_subgraph_errors: + all: true diff --git a/apollo-router/tests/samples/basic/query2/plan.json b/apollo-router/tests/samples/basic/query2/plan.json new file mode 100644 index 0000000000..33f6163a04 --- /dev/null +++ b/apollo-router/tests/samples/basic/query2/plan.json @@ -0,0 +1,26 @@ +{ + "actions": [ + { + "type": "Start", + "schema_path": "./supergraph.graphql", + "configuration_path": "./configuration.yaml", + "subgraphs": {} + }, + { + "type": "Request", + "request": { + "query": "{ me { name } }" + }, + "expected_response": { + "data":{ + "me":{ + "name":"Ada Lovelace" + } + } + } + }, + { + "type": "Stop" + } + ] +} \ No newline at end of file diff --git a/apollo-router/tests/samples/basic/query2/supergraph.graphql b/apollo-router/tests/samples/basic/query2/supergraph.graphql new file mode 100644 index 0000000000..971174056a --- /dev/null +++ b/apollo-router/tests/samples/basic/query2/supergraph.graphql @@ -0,0 +1,90 @@ + +schema + @core(feature: "https://specs.apollo.dev/core/v0.2"), + @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) + @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) +{ + query: Query + mutation: Mutation +} + +directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION + +directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE + +directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION + +directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + +enum core__Purpose { + """ + `EXECUTION` features provide metadata necessary to for operation execution. + """ + EXECUTION + + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY +} + +scalar join__FieldSet + +enum join__Graph { + ACCOUNTS @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") + INVENTORY @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") + PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") + REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") +} +type Mutation { + createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) + createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +} + +type Product + @join__owner(graph: PRODUCTS) + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: REVIEWS, key: "upc") +{ + inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + name: String @join__field(graph: PRODUCTS) + price: Int @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) + reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + upc: String! @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: PRODUCTS) +} + +type Query { + me: User @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) +} + +type Review + @join__owner(graph: REVIEWS) + @join__type(graph: REVIEWS, key: "id") +{ + author: User @join__field(graph: REVIEWS, provides: "username") + body: String @join__field(graph: REVIEWS) + id: ID! @join__field(graph: REVIEWS) + product: Product @join__field(graph: REVIEWS) +} + +type User + @join__owner(graph: ACCOUNTS) + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") +{ + id: ID! @join__field(graph: ACCOUNTS) + name: String @join__field(graph: ACCOUNTS) + reviews: [Review] @join__field(graph: REVIEWS) + username: String @join__field(graph: ACCOUNTS) +} diff --git a/apollo-router/tests/samples_tests.rs b/apollo-router/tests/samples_tests.rs new file mode 100644 index 0000000000..d7772ab41c --- /dev/null +++ b/apollo-router/tests/samples_tests.rs @@ -0,0 +1,429 @@ +use std::collections::HashMap; +use std::env; +use std::error::Error; +use std::fmt::Write; +use std::fs; +use std::fs::File; +use std::io::Read; +use std::net::SocketAddr; +use std::net::TcpListener; +use std::path::Path; +use std::path::PathBuf; +use std::process::ExitCode; + +use libtest_mimic::Arguments; +use libtest_mimic::Failed; +use libtest_mimic::Trial; +use serde::Deserialize; +use serde_json::Value; +use tokio::runtime::Runtime; +use wiremock::matchers::body_partial_json; +use wiremock::Mock; +use wiremock::MockServer; +use wiremock::ResponseTemplate; + +#[path = "./common.rs"] +pub(crate) mod common; +pub(crate) use common::IntegrationTest; + +fn main() -> Result> { + let args = Arguments::from_args(); + let mut tests = Vec::new(); + let path = env::current_dir()?.join("tests/samples"); + + lookup_dir(&path, "", &mut tests)?; + + Ok(libtest_mimic::run(&args, tests).exit_code()) +} + +fn lookup_dir( + path: &Path, + name_prefix: &str, + tests: &mut Vec, +) -> Result<(), Box> { + for entry in fs::read_dir(path)? { + let entry = entry?; + + if entry.file_type()?.is_dir() { + let path = entry.path(); + let name = format!( + "{name_prefix}/{}", + path.file_name().unwrap().to_str().unwrap() + ); + + if path.join("plan.json").exists() { + tests.push(Trial::test(name, move || test(&path))); + } else { + lookup_dir(&path, &name, tests)?; + } + } + } + + Ok(()) +} + +fn test(path: &PathBuf) -> Result<(), Failed> { + //libtest_mimic does not support stdout capture + let mut out = String::new(); + writeln!(&mut out, "test at path: {path:?}").unwrap(); + if let Ok(file) = open_file(&path.join("README.md"), &mut out) { + writeln!(&mut out, "{file}\n\n============\n\n").unwrap(); + } + + let plan: Plan = match serde_json::from_str(&open_file(&path.join("plan.json"), &mut out)?) { + Ok(data) => data, + Err(e) => { + writeln!(&mut out, "could not deserialize test plan: {e}").unwrap(); + return Err(out.into()); + } + }; + + let rt = Runtime::new()?; + + // Spawn the root task + rt.block_on(async { + let mut execution = TestExecution::new(); + for action in plan.actions { + execution.execute_action(&action, path, &mut out).await?; + } + + Ok(()) + }) +} + +struct TestExecution { + router: Option, + subgraphs_server: Option, + subgraphs: HashMap, +} + +impl TestExecution { + fn new() -> Self { + TestExecution { + router: None, + subgraphs_server: None, + subgraphs: HashMap::new(), + } + } + + async fn execute_action( + &mut self, + action: &Action, + path: &Path, + out: &mut String, + ) -> Result<(), Failed> { + match action { + Action::Start { + schema_path, + configuration_path, + subgraphs, + } => { + self.start(schema_path, configuration_path, subgraphs, path, out) + .await + } + Action::ReloadConfiguration { configuration_path } => { + self.reload_configuration(configuration_path, path, out) + .await + } + Action::ReloadSchema { schema_path } => { + self.reload_schema(schema_path, path, out).await + } + Action::Request { + request, + query_path, + expected_response, + } => { + self.request( + request.clone(), + query_path.as_deref(), + expected_response, + path, + out, + ) + .await + } + Action::Stop => self.stop(out).await, + } + } + + async fn start( + &mut self, + schema_path: &str, + configuration_path: &str, + subgraphs: &HashMap, + 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 SubgraphRequest { request, response } in &subgraph.requests { + Mock::given(body_partial_json(request)) + .respond_with(ResponseTemplate::new(200).set_body_json(response)) + .mount(&subgraphs_server) + .await; + } + + // Add a default override for products, if not specified + subgraph_overrides + .entry(name.to_string()) + .or_insert(url.clone()); + } + + let config = open_file(&path.join(configuration_path), out)?; + let schema_path = path.join(schema_path); + check_path(&schema_path, out)?; + + let mut router = IntegrationTest::builder() + .config(&config) + .supergraph(schema_path) + .subgraph_overrides(subgraph_overrides) + .build() + .await; + router.start().await; + router.assert_started().await; + + self.router = Some(router); + self.subgraphs_server = Some(subgraphs_server); + self.subgraphs = subgraphs.clone(); + + Ok(()) + } + + async fn reload_configuration( + &mut self, + configuration_path: &str, + path: &Path, + out: &mut String, + ) -> Result<(), Failed> { + let router = match self.router.as_mut() { + None => { + writeln!( + out, + "cannot reload router configuration: router was not started" + ) + .unwrap(); + return Err(out.into()); + } + Some(router) => router, + }; + + 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 &self.subgraphs { + for SubgraphRequest { request, response } in &subgraph.requests { + Mock::given(body_partial_json(request)) + .respond_with(ResponseTemplate::new(200).set_body_json(response)) + .mount(&subgraphs_server) + .await; + } + + // Add a default override for products, if not specified + subgraph_overrides + .entry(name.to_string()) + .or_insert(url.clone()); + } + + let config = open_file(&path.join(configuration_path), out)?; + + router.update_config(&config).await; + router.assert_reloaded().await; + + Ok(()) + } + + async fn reload_schema( + &mut self, + schema_path: &str, + path: &Path, + out: &mut String, + ) -> Result<(), Failed> { + let router = match self.router.as_mut() { + None => { + writeln!( + out, + "cannot reload router configuration: router was not started" + ) + .unwrap(); + return Err(out.into()); + } + Some(router) => router, + }; + + let schema_path = path.join(schema_path); + + router.update_schema(&schema_path).await; + router.assert_reloaded().await; + + Ok(()) + } + + async fn stop(&mut self, out: &mut String) -> Result<(), Failed> { + if let Some(mut router) = self.router.take() { + router.graceful_shutdown().await; + Ok(()) + } else { + writeln!(out, "could not shutdown router: router was not started").unwrap(); + Err(out.into()) + } + } + + async fn request( + &mut self, + mut request: Value, + query_path: Option<&str>, + expected_response: &Value, + path: &Path, + out: &mut String, + ) -> Result<(), Failed> { + let router = match self.router.as_mut() { + None => { + writeln!( + out, + "cannot send request to the router: router was not started" + ) + .unwrap(); + return Err(out.into()); + } + Some(router) => router, + }; + + if let Some(query_path) = query_path { + let query: String = open_file(&path.join(query_path), out)?; + if let Some(req) = request.as_object_mut() { + req.insert("query".to_string(), query.into()); + } + } + + writeln!(out, "query: {}\n", serde_json::to_string(&request).unwrap()).unwrap(); + let (_, response) = router.execute_query(&request).await; + let body = response.bytes().await.map_err(|e| { + writeln!(out, "could not get graphql response data: {e}").unwrap(); + let f: Failed = out.clone().into(); + f + })?; + let graphql_response: Value = serde_json::from_slice(&body).map_err(|e| { + writeln!(out, "could not deserialize graphql response data: {e}").unwrap(); + let f: Failed = out.clone().into(); + f + })?; + + if expected_response != &graphql_response { + if let Some(requests) = self + .subgraphs_server + .as_ref() + .unwrap() + .received_requests() + .await + { + writeln!(out, "subgraphs received requests:").unwrap(); + for request in requests { + writeln!(out, "\t{}\n", std::str::from_utf8(&request.body).unwrap()).unwrap(); + } + } else { + writeln!(out, "subgraphs received no requests").unwrap(); + } + + writeln!(out, "assertion `left == right` failed").unwrap(); + writeln!( + out, + " left: {}", + serde_json::to_string(&expected_response).unwrap() + ) + .unwrap(); + writeln!( + out, + "right: {}", + serde_json::to_string(&graphql_response).unwrap() + ) + .unwrap(); + return Err(out.into()); + } + + Ok(()) + } +} + +fn open_file(path: &Path, out: &mut String) -> Result { + let mut file = File::open(path).map_err(|e| { + writeln!(out, "could not open file at path '{path:?}': {e}").unwrap(); + let f: Failed = out.into(); + f + })?; + + let mut s = String::new(); + file.read_to_string(&mut s).map_err(|e| { + writeln!(out, "could not read file at path: '{path:?}': {e} ").unwrap(); + let f: Failed = out.into(); + f + })?; + Ok(s) +} + +fn check_path(path: &Path, out: &mut String) -> Result<(), Failed> { + if !path.is_file() { + writeln!(out, "could not find file at path: {path:?}").unwrap(); + return Err(out.into()); + } + Ok(()) +} + +#[derive(Deserialize)] +struct Plan { + actions: Vec, +} + +#[derive(Deserialize)] +#[serde(tag = "type")] +enum Action { + Start { + schema_path: String, + configuration_path: String, + subgraphs: HashMap, + }, + ReloadConfiguration { + configuration_path: String, + }, + ReloadSchema { + schema_path: String, + }, + Request { + request: Value, + query_path: Option, + expected_response: Value, + }, + Stop, +} + +#[derive(Clone, Deserialize)] +struct Subgraph { + requests: Vec, +} + +#[derive(Clone, Deserialize)] +struct SubgraphRequest { + request: Value, + response: Value, +} From 131f5be2c9e4633e9c86b001273ffa324112e841 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Thu, 16 May 2024 21:27:26 +0200 Subject: [PATCH 03/10] Automate bumping the version number of apollo-federation (#5187) Co-authored-by: Coenen Benjamin --- RELEASE_CHECKLIST.md | 14 +++++--------- xtask/src/commands/release.rs | 33 ++++++++++++--------------------- 2 files changed, 17 insertions(+), 30 deletions(-) diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md index f6f2638694..65ddc649ae 100644 --- a/RELEASE_CHECKLIST.md +++ b/RELEASE_CHECKLIST.md @@ -176,29 +176,25 @@ Start following the steps below to start a release PR. The process is **not ful - Run our compliance checks and update the `licenses.html` file as appropriate. - Ensure we're not using any incompatible licenses in the release. -7. Until the script above is updated to do automate this step, - increment the version number in `apollo-federation/Cargo.toml` to match - that of `apollo-router/Cargo.toml`. - -8. Now, review and stage he changes produced by the previous step. This is most safely done using the `--patch` (or `-p`) flag to `git add` (`-u` ignores untracked files). +7. Now, review and stage the changes produced by the previous step. This is most safely done using the `--patch` (or `-p`) flag to `git add` (`-u` ignores untracked files). ``` git add -up . ``` -9. Now commit those changes locally, using a brief message: +8. Now commit those changes locally, using a brief message: ``` git commit -m "prep release: v${APOLLO_ROUTER_RELEASE_VERSION}${APOLLO_ROUTER_PRERELEASE_SUFFIX}" ``` -10. Push this commit up to the existing release PR: +9. Push this commit up to the existing release PR: ``` git push "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "${APOLLO_ROUTER_RELEASE_VERSION}" ``` -11. Git tag the current commit and & push the branch and the pre-release tag simultaneously: +10. Git tag the current commit and & push the branch and the pre-release tag simultaneously: This process will kick off the bulk of the release process on CircleCI, including building each architecture on its own infrastructure and notarizing the macOS binary. @@ -207,7 +203,7 @@ Start following the steps below to start a release PR. The process is **not ful git push "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "${APOLLO_ROUTER_RELEASE_VERSION}" "v${APOLLO_ROUTER_RELEASE_VERSION}${APOLLO_ROUTER_PRERELEASE_SUFFIX}" ``` -12. Finally, publish the Crates from your local computer (this also needs to be moved to CI, but requires changing the release containers to be Rust-enabled and to restore the caches): +11. Finally, publish the Crates from your local computer (this also needs to be moved to CI, but requires changing the release containers to be Rust-enabled and to restore the caches): > Note: This command may appear unnecessarily specific, but it will help avoid publishing a version to Crates.io that doesn't match what you're currently releasing. (e.g., in the event that you've changed branches in another window) diff --git a/xtask/src/commands/release.rs b/xtask/src/commands/release.rs index 61217cf9d3..03440a7e7f 100644 --- a/xtask/src/commands/release.rs +++ b/xtask/src/commands/release.rs @@ -174,29 +174,17 @@ impl Prepare { /// Update the `apollo-router` version in the `dependencies` sections of the `Cargo.toml` files in `apollo-router-scaffold/templates/**`. fn update_cargo_tomls(&self, version: &Version) -> Result { println!("updating Cargo.toml files"); + fn bump(component: &str) -> Result<()> { + for package in ["apollo-federation", "apollo-router"] { + cargo!(["set-version", "--bump", component, "--package", package]); + } + Ok(()) + } match version { Version::Current => {} - Version::Major => cargo!([ - "set-version", - "--bump", - "major", - "--package", - "apollo-router" - ]), - Version::Minor => cargo!([ - "set-version", - "--bump", - "minor", - "--package", - "apollo-router" - ]), - Version::Patch => cargo!([ - "set-version", - "--bump", - "patch", - "--package", - "apollo-router" - ]), + Version::Major => bump("major")?, + Version::Minor => bump("minor")?, + Version::Patch => bump("patch")?, Version::Nightly => { // Get the first 8 characters of the current commit hash by running // the Command::new("git") command. Be sure to take the output and @@ -231,6 +219,9 @@ impl Prepare { ); } Version::Version(version) => { + // Also updates apollo-router's dependency: + cargo!(["set-version", version, "--package", "apollo-federation"]); + cargo!(["set-version", version, "--package", "apollo-router"]) } } From 95e7a34e0ea1144ec2c43ab7475fd990488521ef Mon Sep 17 00:00:00 2001 From: Tyler Bloom Date: Thu, 16 May 2024 17:52:27 -0400 Subject: [PATCH 04/10] Ported computeRootSerialDependencyGraph (#5170) --- apollo-federation/src/query_graph/mod.rs | 2 +- .../src/query_plan/fetch_dependency_graph.rs | 10 ++ apollo-federation/src/query_plan/operation.rs | 38 ++++- .../src/query_plan/query_planner.rs | 135 +++++++++++++++++- .../query_plan/query_planning_traversal.rs | 38 +---- 5 files changed, 182 insertions(+), 41 deletions(-) diff --git a/apollo-federation/src/query_graph/mod.rs b/apollo-federation/src/query_graph/mod.rs index dd29e5acff..9f872e7644 100644 --- a/apollo-federation/src/query_graph/mod.rs +++ b/apollo-federation/src/query_graph/mod.rs @@ -104,7 +104,7 @@ impl Display for QueryGraphNodeType { } } -#[derive(Debug, Clone)] +#[derive(Debug, PartialEq, Clone)] pub(crate) struct QueryGraphEdge { /// Indicates what kind of edge this is and what the edge does/represents. For instance, if the /// edge represents a field, the `transition` will be a `FieldCollection` transition and will diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index 4710c26ad2..c1a0b49e48 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -488,6 +488,16 @@ impl FetchDependencyGraph { } } + pub(crate) fn next_fetch_id(&self) -> u64 { + self.fetch_id_generation.next_id() + } + + pub(crate) fn root_node_by_subgraph_iter( + &self, + ) -> impl Iterator { + self.root_nodes_by_subgraph.iter() + } + /// Must be called every time the "shape" of the graph is modified /// to know that the graph may not be minimal/optimized anymore. fn on_modification(&mut self) { diff --git a/apollo-federation/src/query_plan/operation.rs b/apollo-federation/src/query_plan/operation.rs index 02ce57356e..34d8ab26f1 100644 --- a/apollo-federation/src/query_plan/operation.rs +++ b/apollo-federation/src/query_plan/operation.rs @@ -685,7 +685,7 @@ impl Containment { /// An analogue of the apollo-compiler type `Selection` that stores our other selection analogues /// instead of the apollo-compiler types. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, derive_more::IsVariant)] pub(crate) enum Selection { Field(Arc), FragmentSpread(Arc), @@ -1913,6 +1913,42 @@ impl SelectionSet { } } + // TODO: Ideally, this method returns a proper, recursive iterator. As is, there is a lot of + // overhead due to indirection, both from over allocation and from v-table lookups. + pub(crate) fn split_top_level_fields(self) -> Box> { + let parent_type = self.type_position.clone(); + let selections: IndexMap = (**self.selections).clone(); + Box::new(selections.into_values().flat_map(move |sel| { + let digest: Box> = if sel.is_field() { + Box::new(std::iter::once(SelectionSet::from_selection( + parent_type.clone(), + sel.clone(), + ))) + } else { + let Some(ele) = sel.element().ok() else { + let digest: Box> = + Box::new(std::iter::empty()); + return digest; + }; + Box::new( + sel.selection_set() + .ok() + .flatten() + .cloned() + .into_iter() + .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)) + .ok() + .map(|sel| SelectionSet::from_selection(parent_type, sel)) + }), + ) + }; + digest + })) + } + /// PORT_NOTE: JS calls this `newCompositeTypeSelectionSet` pub(crate) fn for_composite_type( schema: ValidFederationSchema, diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index 55d6f225c7..a062fe751d 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -8,6 +8,8 @@ use apollo_compiler::ExecutableDocument; use apollo_compiler::NodeStr; use indexmap::IndexMap; use indexmap::IndexSet; +use petgraph::csr::NodeIndex; +use petgraph::stable_graph::IndexType; use crate::error::FederationError; use crate::error::SingleFederationError; @@ -15,7 +17,10 @@ use crate::link::federation_spec_definition::FederationSpecDefinition; use crate::link::federation_spec_definition::FEDERATION_INTERFACEOBJECT_DIRECTIVE_NAME_IN_SPEC; use crate::link::spec::Identity; use crate::query_graph::build_federated_query_graph; +use crate::query_graph::path_tree::OpPathTree; use crate::query_graph::QueryGraph; +use crate::query_graph::QueryGraphNodeType; +use crate::query_plan::fetch_dependency_graph::compute_nodes_for_tree; use crate::query_plan::fetch_dependency_graph::FetchDependencyGraph; use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphProcessor; use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphToCostProcessor; @@ -34,8 +39,10 @@ use crate::query_plan::QueryPlan; use crate::query_plan::SequenceNode; use crate::query_plan::TopLevelPlanNode; use crate::schema::position::AbstractTypeDefinitionPosition; +use crate::schema::position::CompositeTypeDefinitionPosition; use crate::schema::position::InterfaceTypeDefinitionPosition; use crate::schema::position::ObjectTypeDefinitionPosition; +use crate::schema::position::OutputTypeDefinitionPosition; use crate::schema::position::SchemaRootDefinitionKind; use crate::schema::position::TypeDefinitionPosition; use crate::schema::ValidFederationSchema; @@ -490,10 +497,132 @@ impl QueryPlanner { } fn compute_root_serial_dependency_graph( - _parameters: &QueryPlanningParameters, - _has_defers: bool, + parameters: &QueryPlanningParameters, + has_defers: bool, ) -> Result, FederationError> { - todo!("FED-127") + let QueryPlanningParameters { + supergraph_schema, + federated_query_graph, + operation, + .. + } = parameters; + let root_type: Option = if has_defers { + supergraph_schema + .schema() + .root_operation(operation.root_kind.into()) + .and_then(|name| supergraph_schema.get_type(name.clone()).ok()) + .and_then(|ty| ty.try_into().ok()) + } else { + None + }; + // 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"))?; + let BestQueryPlanInfo { + mut fetch_dependency_graph, + path_tree: mut prev_path, + .. + } = compute_root_parallel_best_plan(parameters, selection_set, has_defers)?; + let mut prev_subgraph = only_root_subgraph(&fetch_dependency_graph)?; + for selection_set in split_roots { + let BestQueryPlanInfo { + fetch_dependency_graph: new_dep_graph, + path_tree: new_path, + .. + } = compute_root_parallel_best_plan(parameters, selection_set, has_defers)?; + let new_subgraph = only_root_subgraph(&new_dep_graph)?; + if new_subgraph == prev_subgraph { + // The new operation (think 'mutation' operation) is on the same subgraph than the previous one, so we can concat them in a single fetch + // and rely on the subgraph to enforce seriability. Do note that we need to `concat()` and not `merge()` because if we have + // mutation Mut { + // mut1 {...} + // mut2 {...} + // mut1 {...} + // } + // then we should _not_ merge the 2 `mut1` fields (contrarily to what happens on queried fields). + + prev_path = OpPathTree::merge(&prev_path, &new_path); + fetch_dependency_graph = FetchDependencyGraph::new( + supergraph_schema.clone(), + federated_query_graph.clone(), + root_type.clone(), + starting_fetch_id, + ); + compute_root_fetch_groups( + operation.root_kind, + &mut fetch_dependency_graph, + &prev_path, + )?; + } else { + // PORT_NOTE: It is unclear if they correct thing to do here is get the next ID, use + // 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, + )); + prev_path = new_path; + prev_subgraph = new_subgraph; + } + } + digest.push(fetch_dependency_graph); + Ok(digest) +} + +fn only_root_subgraph(graph: &FetchDependencyGraph) -> Result { + let mut iter = graph.root_node_by_subgraph_iter(); + let (Some((_, index)), None) = (iter.next(), iter.next()) else { + return Err(FederationError::internal(format!( + "{graph} should have only one root." + ))); + }; + Ok(index.index() as u32) +} + +pub(crate) fn compute_root_fetch_groups( + root_kind: SchemaRootDefinitionKind, + dependency_graph: &mut FetchDependencyGraph, + path: &OpPathTree, +) -> Result<(), FederationError> { + // The root of the pathTree is one of the "fake" root of the subgraphs graph, + // which belongs to no subgraph but points to each ones. + // So we "unpack" the first level of the tree to find out our top level groups + // (and initialize our stack). + // Note that we can safely ignore the triggers of that first level + // as it will all be free transition, and we know we cannot have conditions. + for child in &path.childs { + let edge = child.edge.expect("The root edge should not be None"); + let (_source_node, target_node) = path.graph.edge_endpoints(edge)?; + let target_node = path.graph.node_weight(target_node)?; + let subgraph_name = &target_node.source; + let root_type = match &target_node.type_ { + QueryGraphNodeType::SchemaType(OutputTypeDefinitionPosition::Object(object)) => { + object.clone().into() + } + ty => { + return Err(FederationError::internal(format!( + "expected an object type for the root of a subgraph, found {ty}" + ))) + } + }; + let fetch_dependency_node = + dependency_graph.get_or_create_root_node(subgraph_name, root_kind, root_type)?; + compute_nodes_for_tree( + dependency_graph, + &child.tree, + fetch_dependency_node, + Default::default(), + Default::default(), + &Default::default(), + )?; + } + Ok(()) } fn compute_root_parallel_dependency_graph( diff --git a/apollo-federation/src/query_plan/query_planning_traversal.rs b/apollo-federation/src/query_plan/query_planning_traversal.rs index 5fe3f3b125..aa3f5db2d7 100644 --- a/apollo-federation/src/query_plan/query_planning_traversal.rs +++ b/apollo-federation/src/query_plan/query_planning_traversal.rs @@ -33,13 +33,13 @@ use crate::query_plan::generate::PlanBuilder; use crate::query_plan::operation::Operation; use crate::query_plan::operation::Selection; use crate::query_plan::operation::SelectionSet; +use crate::query_plan::query_planner::compute_root_fetch_groups; use crate::query_plan::query_planner::QueryPlannerConfig; use crate::query_plan::query_planner::QueryPlanningStatistics; use crate::query_plan::QueryPlanCost; use crate::schema::position::AbstractTypeDefinitionPosition; use crate::schema::position::CompositeTypeDefinitionPosition; use crate::schema::position::ObjectTypeDefinitionPosition; -use crate::schema::position::OutputTypeDefinitionPosition; use crate::schema::position::SchemaRootDefinitionKind; use crate::schema::ValidFederationSchema; @@ -844,41 +844,7 @@ impl<'a> QueryPlanningTraversal<'a> { QueryGraphNodeType::FederatedRootType(_) ); if is_root_path_tree { - // The root of the pathTree is one of the "fake" root of the subgraphs graph, - // which belongs to no subgraph but points to each ones. - // So we "unpack" the first level of the tree to find out our top level groups - // (and initialize our stack). - // Note that we can safely ignore the triggers of that first level - // as it will all be free transition, and we know we cannot have conditions. - for child in &path_tree.childs { - let edge = child.edge.expect("The root edge should not be None"); - let (_source_node, target_node) = path_tree.graph.edge_endpoints(edge)?; - let target_node = path_tree.graph.node_weight(target_node)?; - let subgraph_name = &target_node.source; - let root_type = match &target_node.type_ { - QueryGraphNodeType::SchemaType(OutputTypeDefinitionPosition::Object( - object, - )) => object.clone().into(), - ty => { - return Err(FederationError::internal(format!( - "expected an object type for the root of a subgraph, found {ty}" - ))) - } - }; - let fetch_dependency_node = dependency_graph.get_or_create_root_node( - subgraph_name, - self.root_kind, - root_type, - )?; - compute_nodes_for_tree( - dependency_graph, - &child.tree, - fetch_dependency_node, - Default::default(), - Default::default(), - &Default::default(), - )?; - } + compute_root_fetch_groups(self.root_kind, dependency_graph, path_tree)?; } else { let query_graph_node = path_tree.graph.node_weight(path_tree.node)?; let subgraph_name = &query_graph_node.source; From 2beab74703d2f763cbff34908754a9219bd50155 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Fri, 17 May 2024 17:11:31 +0200 Subject: [PATCH 05/10] Port some more QP tests (#5169) Co-authored-by: Iryna Shestak --- .gitignore | 3 + apollo-federation/src/error/mod.rs | 40 +- apollo-federation/src/query_plan/display.rs | 3 +- .../src/query_plan/fetch_dependency_graph.rs | 22 +- apollo-federation/src/query_plan/operation.rs | 16 +- .../src/query_plan/query_planner.rs | 12 +- .../query_plan/build_query_plan_support.rs | 17 +- .../query_plan/build_query_plan_tests.rs | 181 +++ .../fetch_operation_names.rs | 288 ++++ .../build_query_plan_tests/provides.rs | 855 ++++++++++ .../build_query_plan_tests/requires.rs | 1406 +++++++++++++++++ .../requires/include_skip.rs | 244 +++ ...here_is_too_many_plans_to_consider.graphql | 70 + ...es_sanitization_applies_repeatedly.graphql | 57 + ...ield_covariance_and_type_explosion.graphql | 65 + ...le_subgraph_with_hypen_in_the_name.graphql | 57 + ...andle_very_non_graph_subgraph_name.graphql | 57 + ...res_involving_different_nestedness.graphql | 69 + ...iding_fields_for_only_some_subtype.graphql | 78 + ...can_require_at_inaccessible_fields.graphql | 65 + ...t_handes_diamond_shape_depedencies.graphql | 68 + ...res_triggered_within_a_conditional.graphql | 58 + ...t_requires_triggered_conditionally.graphql | 58 + ..._multiple_conditional_are_involved.graphql | 68 + .../it_handles_complex_require_chain.graphql | 96 ++ .../it_handles_longer_require_chain.graphql | 90 ++ ...uires_within_the_same_entity_fetch.graphql | 86 + ...chain_not_ending_in_original_group.graphql | 65 + .../it_handles_simple_require_chain.graphql | 62 + ...one_is_also_a_key_to_reach_another.graphql | 63 + .../it_works_on_interfaces.graphql | 82 + .../supergraphs/it_works_on_unions.graphql | 74 + .../it_works_with_nested_provides.graphql | 70 + ..._only_reachable_by_the_at_provides.graphql | 87 + .../pick_keys_that_minimize_fetches.graphql | 73 + .../src/query_planner/bridge_query_planner.rs | 22 +- 36 files changed, 4677 insertions(+), 50 deletions(-) create mode 100644 apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs create mode 100644 apollo-federation/tests/query_plan/build_query_plan_tests/provides.rs create mode 100644 apollo-federation/tests/query_plan/build_query_plan_tests/requires.rs create mode 100644 apollo-federation/tests/query_plan/build_query_plan_tests/requires/include_skip.rs create mode 100644 apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql create mode 100644 apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql diff --git a/.gitignore b/.gitignore index c8cb2b01a8..14a36f867f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ **/target/ .cargo_check +# snapshot testing with the 'insta' crate +*.pending-snap + # These are backup files generated by rustfmt **/*.rs.bk diff --git a/apollo-federation/src/error/mod.rs b/apollo-federation/src/error/mod.rs index 881b2b395b..317f7402d2 100644 --- a/apollo-federation/src/error/mod.rs +++ b/apollo-federation/src/error/mod.rs @@ -1,3 +1,4 @@ +use std::backtrace::Backtrace; use std::cmp::Ordering; use std::fmt::Display; use std::fmt::Formatter; @@ -393,8 +394,8 @@ pub struct MultipleFederationErrors { impl MultipleFederationErrors { pub fn push(&mut self, error: FederationError) { match error { - FederationError::SingleFederationError(error) => { - self.errors.push(error); + FederationError::SingleFederationError { inner, .. } => { + self.errors.push(inner); } FederationError::MultipleFederationErrors(errors) => { self.errors.extend(errors.errors); @@ -468,20 +469,47 @@ impl Display for AggregateFederationError { } } +/// Work around thiserror, which when an error field has a type named `Backtrace` +/// "helpfully" implements `Error::provides` even though that API is not stable yet: +/// +type ThiserrorTrustMeThisIsTotallyNotABacktrace = Backtrace; + // PORT_NOTE: Often times, JS functions would either throw/return a GraphQLError, return a vector // of GraphQLErrors, or take a vector of GraphQLErrors and group them together under an // AggregateGraphQLError which itself would have a specific error message and code, and throw that. // We represent all these cases with an enum, and delegate to the members. -#[derive(Debug, Clone, thiserror::Error)] +#[derive(thiserror::Error)] pub enum FederationError { - #[error(transparent)] - SingleFederationError(#[from] SingleFederationError), + #[error("{inner}")] + SingleFederationError { + inner: SingleFederationError, + trace: ThiserrorTrustMeThisIsTotallyNotABacktrace, + }, #[error(transparent)] MultipleFederationErrors(#[from] MultipleFederationErrors), #[error(transparent)] AggregateFederationError(#[from] AggregateFederationError), } +impl std::fmt::Debug for FederationError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::SingleFederationError { inner, trace } => write!(f, "{inner}\n{trace}"), + Self::MultipleFederationErrors(inner) => std::fmt::Debug::fmt(inner, f), + Self::AggregateFederationError(inner) => std::fmt::Debug::fmt(inner, f), + } + } +} + +impl From for FederationError { + fn from(inner: SingleFederationError) -> Self { + Self::SingleFederationError { + inner, + trace: Backtrace::capture(), + } + } +} + impl From for FederationError { fn from(value: DiagnosticList) -> Self { let value: MultipleFederationErrors = value.into(); @@ -496,7 +524,7 @@ impl From> for FederationError { } impl FederationError { - pub(crate) fn internal(message: impl Into) -> Self { + pub fn internal(message: impl Into) -> Self { SingleFederationError::Internal { message: message.into(), } diff --git a/apollo-federation/src/query_plan/display.rs b/apollo-federation/src/query_plan/display.rs index da81fd057d..8c91f3eac1 100644 --- a/apollo-federation/src/query_plan/display.rs +++ b/apollo-federation/src/query_plan/display.rs @@ -354,11 +354,12 @@ fn write_selections( state.write("}") } +/// PORT_NOTE: Corresponds to `GroupPath.updatedResponsePath` in `buildPlan.ts` impl fmt::Display for FetchDataPathElement { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Key(name) => f.write_str(name), - Self::AnyIndex => f.write_str("*"), + Self::AnyIndex => f.write_str("@"), Self::TypenameEquals(name) => write!(f, "... on {name}"), } } diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index c1a0b49e48..c23054a4e8 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -1460,18 +1460,17 @@ fn operation_for_entities_fetch( ); let query_type_name = subgraph_schema.schema().root_operation(OperationType::Query).ok_or_else(|| - FederationError::SingleFederationError(SingleFederationError::InvalidGraphQL { + SingleFederationError::InvalidGraphQL { message: "Subgraphs should always have a query root (they should at least provides _entities)".to_string() - }))?; + })?; let query_type = match subgraph_schema.get_type(query_type_name.clone())? { crate::schema::position::TypeDefinitionPosition::Object(o) => o, _ => { - return Err(FederationError::SingleFederationError( - SingleFederationError::InvalidGraphQL { - message: "the root query type must be an object".to_string(), - }, - )) + return Err(SingleFederationError::InvalidGraphQL { + message: "the root query type must be an object".to_string(), + } + .into()) } }; @@ -1480,11 +1479,10 @@ fn operation_for_entities_fetch( .fields .contains_key(&ENTITIES_QUERY) { - return Err(FederationError::SingleFederationError( - SingleFederationError::InvalidGraphQL { - message: "Subgraphs should always have the _entities field".to_string(), - }, - )); + return Err(SingleFederationError::InvalidGraphQL { + message: "Subgraphs should always have the _entities field".to_string(), + } + .into()); } let entities = FieldDefinitionPosition::Object(query_type.field(ENTITIES_QUERY.clone())); diff --git a/apollo-federation/src/query_plan/operation.rs b/apollo-federation/src/query_plan/operation.rs index 34d8ab26f1..1630545b57 100644 --- a/apollo-federation/src/query_plan/operation.rs +++ b/apollo-federation/src/query_plan/operation.rs @@ -2406,12 +2406,12 @@ impl SelectionSet { } SelectionValue::FragmentSpread(fragment_spread) => { // at this point in time all fragment spreads should have been converted into inline fragments - return Err(FederationError::SingleFederationError(Internal { - message: format!( + return Err(FederationError::internal( + format!( "Error while optimizing sibling typename information, selection set contains {} named fragment", fragment_spread.get().spread.data().fragment_name - ), - })); + ) + )); } } } @@ -3425,11 +3425,9 @@ pub(crate) fn subselection_type_if_abstract( r.original_fragments .get(&fragment_spread.spread.data().fragment_name) }) - .ok_or(FederationError::SingleFederationError( - crate::error::SingleFederationError::InvalidGraphQL { - message: "missing fragment".to_string(), - }, - )) + .ok_or(crate::error::SingleFederationError::InvalidGraphQL { + message: "missing fragment".to_string(), + }) //FIXME: return error .ok()?; match fragment.type_condition_position.clone() { diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index a062fe751d..8697dd118c 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -338,7 +338,7 @@ impl QueryPlanner { let node = FetchNode { subgraph_name: subgraph_name.clone(), operation_document: document.clone(), - operation_name: operation_name.as_deref().cloned(), + operation_name: operation.name.as_deref().cloned(), operation_kind: operation.operation_type, id: None, variable_usages: operation @@ -415,7 +415,7 @@ impl QueryPlanner { let processor = FetchDependencyGraphToQueryPlanProcessor::new( operation.variables.clone(), Some(RebasedFragments::new(&normalized_operation.named_fragments)), - operation_name.clone(), + operation.name.clone(), assigned_defer_labels, ); let mut parameters = QueryPlanningParameters { @@ -925,7 +925,7 @@ type User }, Parallel { Sequence { - Flatten(path: "bestRatedProducts.*") { + Flatten(path: "bestRatedProducts.@") { Fetch(service: "products") { { ... on Movie { @@ -943,7 +943,7 @@ type User } }, }, - Flatten(path: "bestRatedProducts.*.vendor") { + Flatten(path: "bestRatedProducts.@.vendor") { Fetch(service: "accounts") { { ... on User { @@ -960,7 +960,7 @@ type User }, }, Sequence { - Flatten(path: "bestRatedProducts.*") { + Flatten(path: "bestRatedProducts.@") { Fetch(service: "products") { { ... on Book { @@ -978,7 +978,7 @@ type User } }, }, - Flatten(path: "bestRatedProducts.*.vendor") { + Flatten(path: "bestRatedProducts.@.vendor") { Fetch(service: "accounts") { { ... on User { diff --git a/apollo-federation/tests/query_plan/build_query_plan_support.rs b/apollo-federation/tests/query_plan/build_query_plan_support.rs index 87439fa18a..14b1d71c7e 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_support.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_support.rs @@ -26,7 +26,7 @@ const IMPLICIT_LINK_DIRECTIVE: &str = r#"@link(url: "https://specs.apollo.dev/fe macro_rules! planner { ( $( config = $config: expr, )? - $( $subgraph_name: ident: $subgraph_schema: expr),+ + $( $subgraph_name: tt: $subgraph_schema: expr),+ $(,)? ) => {{ #[allow(unused_mut)] @@ -35,11 +35,20 @@ macro_rules! planner { $crate::query_plan::build_query_plan_support::api_schema_and_planner( insta::_function_name!(), config, - &[ $( (stringify!($subgraph_name), $subgraph_schema) ),+ ], + &[ $( (subgraph_name!($subgraph_name), $subgraph_schema) ),+ ], ) }}; } +macro_rules! subgraph_name { + ($x: ident) => { + stringify!($x) + }; + ($x: literal) => { + $x + }; +} + /// Takes a reference to the result of `planner!()`, an operation string, and an expected /// formatted query plan string. /// Run `cargo insta review` to diff and accept changes to the generated query plan. @@ -52,7 +61,9 @@ macro_rules! assert_plan { "operation.graphql", ) .unwrap(); - insta::assert_snapshot!(planner.build_query_plan(&document, None).unwrap(), @$expected); + let plan = planner.build_query_plan(&document, None).unwrap(); + insta::assert_snapshot!(plan, @$expected); + plan }}; } diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests.rs b/apollo-federation/tests/query_plan/build_query_plan_tests.rs index 121d99fb73..4c8bf1f13b 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests.rs @@ -31,6 +31,187 @@ fn some_name() { } */ +mod fetch_operation_names; +mod provides; +mod requires; mod shareable_root_fields; // TODO: port the rest of query-planner-js/src/__tests__/buildPlan.test.ts + +#[test] +fn pick_keys_that_minimize_fetches() { + let planner = planner!( + Subgraph1: r#" + type Query { + transfers: [Transfer!]! + } + + type Transfer @key(fields: "from { iso } to { iso }") { + from: Country! + to: Country! + } + + type Country @key(fields: "iso") { + iso: String! + } + "#, + Subgraph2: r#" + type Transfer @key(fields: "from { iso } to { iso }") { + id: ID! + from: Country! + to: Country! + } + + type Country @key(fields: "iso") { + iso: String! + currency: Currency! + } + + type Currency { + name: String! + sign: String! + } + "#, + ); + // We want to make sure we use the key on Transfer just once, + // not 2 fetches using the keys on Country. + assert_plan!( + &planner, + r#" + { + transfers { + from { + currency { + name + } + } + to { + currency { + sign + } + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + transfers { + __typename + from { + iso + } + to { + iso + } + } + } + }, + Flatten(path: "transfers.@") { + Fetch(service: "Subgraph2") { + { + ... on Transfer { + __typename + from { + iso + } + to { + iso + } + } + } => + { + ... on Transfer { + from { + currency { + name + } + } + to { + currency { + sign + } + } + } + } + }, + }, + }, + } + "### + ); +} + +/// This tests the issue from https://github.com/apollographql/federation/issues/1858. +/// That issue, which was a bug in the handling of selection sets, was concretely triggered with +/// a mix of an interface field implemented with some covariance and the query plan using +/// type-explosion. +/// That error can be reproduced on a pure fed2 example, it's just a bit more +/// complex as we need to involve a @provide just to force the query planner to type explode +/// (more precisely, this force the query planner to _consider_ type explosion; the generated +/// query plan still ends up not type-exploding in practice since as it's not necessary). +#[test] +#[should_panic(expected = "snapshot assertion")] +// TODO: investigate this failure +fn field_covariance_and_type_explosion() { + let planner = planner!( + Subgraph1: r#" + type Query { + dummy: Interface + } + + interface Interface { + field: Interface + } + + type Object implements Interface @key(fields: "id") { + id: ID! + field: Object @provides(fields: "x") + x: Int @external + } + "#, + Subgraph2: r#" + type Object @key(fields: "id") { + id: ID! + x: Int @shareable + } + "#, + ); + assert_plan!( + &planner, + r#" + { + dummy { + field { + ... on Object { + field { + __typename + } + } + } + } + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + dummy { + __typename + field { + __typename + ... on Object { + field { + __typename + } + } + } + } + } + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs new file mode 100644 index 0000000000..76cccfe5a2 --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs @@ -0,0 +1,288 @@ +use std::fmt::Write; + +use apollo_federation::query_plan::query_planner::QueryPlannerDebugConfig; +use apollo_federation::query_plan::PlanNode; +use apollo_federation::query_plan::QueryPlan; +use apollo_federation::query_plan::TopLevelPlanNode; + +fn second_operation(plan: &QueryPlan) -> String { + let Some(TopLevelPlanNode::Sequence(node)) = &plan.node else { + panic!() + }; + let [_, PlanNode::Flatten(node)] = &*node.nodes else { + panic!() + }; + let PlanNode::Fetch(node) = &*node.node else { + panic!() + }; + node.operation_document.to_string() +} + +macro_rules! assert_starts_with { + ($haystack: expr, $needle: expr) => {{ + let haystack = $haystack; + let needle = $needle; + assert!( + haystack.starts_with(needle), + "{:?}.starts_with({needle:?})", + &haystack[..50] + ); + }}; +} + +#[test] +fn handle_subgraph_with_hypen_in_the_name() { + let planner = planner!( + S1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + "non-graphql-name": r#" + type T @key(fields: "id") { + id: ID! + x: Int + } + "#, + ); + let plan = assert_plan!( + &planner, + r#" + query myOp { + t { + x + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "non-graphql-name") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + x + } + } + }, + }, + }, + } + "### + ); + assert_starts_with!(second_operation(&plan), "query myOp__non_graphql_name__1("); +} + +#[test] +fn ensures_sanitization_applies_repeatedly() { + let planner = planner!( + S1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + "a-na&me-with-plen&ty-replace*ments": r#" + type T @key(fields: "id") { + id: ID! + x: Int + } + "#, + ); + let plan = assert_plan!( + &planner, + r#" + query myOp { + t { + x + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "a-na&me-with-plen&ty-replace*ments") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + x + } + } + }, + }, + }, + } + "### + ); + assert_starts_with!( + second_operation(&plan), + "query myOp__a_name_with_plenty_replacements__1(" + ); +} + +#[test] +fn handle_very_non_graph_subgraph_name() { + let planner = planner!( + S1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + } + "#, + "42!": r#" + type T @key(fields: "id") { + id: ID! + x: Int + } + "#, + ); + let plan = assert_plan!( + &planner, + r#" + query myOp { + t { + x + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "42!") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + x + } + } + }, + }, + }, + } + "### + ); + assert_starts_with!(second_operation(&plan), "query myOp___42__1("); +} + +#[test] +fn correctly_handle_case_where_there_is_too_many_plans_to_consider() { + // Creating realistic examples where there is too many plan to consider is not trivial, but creating unrealistic examples + // is thankfully trivial. Here, we just have 2 subgraphs that are _exactly the same_ with a single type having plenty of + // fields. The reason this create plenty of possible query plans is that each field can be independently reached + // from either subgraph and so in theory the possible plans is the cartesian product of the 2 choices for each field (which + // gets very large very quickly). Obviously, there is no reason to do this in practice. + + // Each leaf field is reachable from 2 subgraphs, so doubles the number of plans. + let default_max_computed_plans = QueryPlannerDebugConfig::default().max_evaluated_plans.get(); + let field_count = (default_max_computed_plans as f64).log2().ceil() as usize + 1; + let mut field_names: Vec<_> = (0..field_count).map(|i| format!("f{i}")).collect(); + let mut schema = r#" + type Query { + t: T @shareable + } + + type T {"# + .to_owned(); + let mut operation = "{\n t {".to_owned(); + for f in &field_names { + write!(&mut schema, "\n {f}: Int @shareable").unwrap(); + write!(&mut operation, "\n {f}").unwrap(); + } + schema.push_str("\n }\n"); + operation.push_str("\n }\n}\n"); + + let (api_schema, planner) = planner!( + S1: &schema, + S2: &schema, + ); + let document = apollo_compiler::ExecutableDocument::parse_and_validate( + api_schema.schema(), + operation, + "operation.graphql", + ) + .unwrap(); + let plan = planner.build_query_plan(&document, None).unwrap(); + + // Note: The way the code that handle multiple plans currently work, it mess up the order of fields a bit. It's not a + // big deal in practice cause everything gets re-order in practice during actual execution, but this means it's a tad + // harder to valid the plan automatically here with `toMatchInlineSnapshot`. + + let Some(TopLevelPlanNode::Fetch(fetch)) = &plan.node else { + panic!() + }; + assert_eq!(fetch.subgraph_name, "S1"); + assert!(fetch.requires.is_none()); + assert!(fetch.operation_document.fragments.is_empty()); + let mut operations = fetch.operation_document.all_operations(); + let operation = operations.next().unwrap(); + assert!(operations.next().is_none()); + // operation is essentially: + // { + // t { + // ... all fields + // } + // } + assert_eq!(operation.selection_set.selections.len(), 1); + let field = operation.selection_set.selections[0].as_field().unwrap(); + let mut names: Vec<_> = field + .selection_set + .selections + .iter() + .map(|sel| sel.as_field().unwrap().name.as_str().to_owned()) + .collect(); + names.sort(); + field_names.sort(); + assert_eq!(names, field_names); +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/provides.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/provides.rs new file mode 100644 index 0000000000..23ba713f10 --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/provides.rs @@ -0,0 +1,855 @@ +#[test] +fn it_works_with_nested_provides() { + let planner = planner!( + Subgraph1: r#" + type Query { + doSomething: Response + doSomethingWithProvides: Response + @provides( + fields: "responseValue { subResponseValue { subSubResponseValue } }" + ) + } + + type Response { + responseValue: SubResponse + } + + type SubResponse { + subResponseValue: SubSubResponse + } + + type SubSubResponse @key(fields: "id") { + id: ID! + subSubResponseValue: Int @external + } + "#, + Subgraph2: r#" + type SubSubResponse @key(fields: "id") { + id: ID! + subSubResponseValue: Int @shareable + } + "#, + ); + // This is our sanity check: we first query _without_ the provides + // to make sure we _do_ need to go the the second subgraph. + assert_plan!( + &planner, + r#" + { + doSomething { + responseValue { + subResponseValue { + subSubResponseValue + } + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + doSomething { + responseValue { + subResponseValue { + __typename + id + } + } + } + } + }, + Flatten(path: "doSomething.responseValue.subResponseValue") { + Fetch(service: "Subgraph2") { + { + ... on SubSubResponse { + __typename + id + } + } => + { + ... on SubSubResponse { + subSubResponseValue + } + } + }, + }, + }, + } + "### + ); + // And now make sure with the provides we do only get a fetch to subgraph1 + assert_plan!( + &planner, + r#" + { + doSomethingWithProvides { + responseValue { + subResponseValue { + subSubResponseValue + } + } + } + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + doSomethingWithProvides { + responseValue { + subResponseValue { + subSubResponseValue + } + } + } + } + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "Schema has no type \"I\"")] // TODO: fix bug FED-230 +fn it_works_on_interfaces() { + let planner = planner!( + Subgraph1: r#" + type Query { + noProvides: I + withProvides: I @provides(fields: "v { a }") + } + + interface I { + v: Value + } + + type Value { + a: Int @shareable + } + + type T1 implements I @key(fields: "id") { + id: ID! + v: Value @external + } + + type T2 implements I @key(fields: "id") { + id: ID! + v: Value @external + } + "#, + Subgraph2: r#" + type Value { + a: Int @shareable + b: Int + } + + type T1 @key(fields: "id") { + id: ID! + v: Value @shareable + } + + type T2 @key(fields: "id") { + id: ID! + v: Value @shareable + } + "#, + ); + // This is our sanity check: we first query _without_ the provides + // to make sure we _do_ need to go the the second subgraph. + assert_plan!( + &planner, + r#" + { + noProvides { + v { + a + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + noProvides { + __typename + ... on T1 { + __typename + id + } + ... on T2 { + __typename + id + } + } + } + }, + Flatten(path: "noProvides") { + Fetch(service: "Subgraph2") { + { + ... on T1 { + __typename + id + } + ... on T2 { + __typename + id + } + } => + { + ... on T1 { + v { + a + } + } + ... on T2 { + v { + a + } + } + } + }, + }, + }, + } + "### + ); + // Ensuring that querying only `a` can be done with subgraph1 only. + assert_plan!( + &planner, + r#" + { + withProvides { + v { + a + } + } + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + withProvides { + __typename + v { + a + } + } + } + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "snapshot assertion")] +// TODO: investigate this failure +fn it_works_on_unions() { + let planner = planner!( + Subgraph1: r#" + type Query { + noProvides: U + withProvidesForT1: U @provides(fields: "... on T1 { a }") + withProvidesForBoth: U + @provides(fields: "... on T1 { a } ... on T2 {b}") + } + + union U = T1 | T2 + + type T1 @key(fields: "id") { + id: ID! + a: Int @external + } + + type T2 @key(fields: "id") { + id: ID! + a: Int + b: Int @external + } + "#, + Subgraph2: r#" + type T1 @key(fields: "id") { + id: ID! + a: Int @shareable + } + + type T2 @key(fields: "id") { + id: ID! + b: Int @shareable + } + "#, + ); + assert_plan!( + &planner, + r#" + { + noProvides { + ... on T1 { + a + } + ... on T2 { + a + b + } + } + } + "#, + // This is our sanity check: we first query _without_ the provides + // to make sure we _do_ need to go the the second subgraph. + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + noProvides { + __typename + ... on T1 { + __typename + id + } + ... on T2 { + __typename + id + a + } + } + } + }, + Flatten(path: "noProvides") { + Fetch(service: "Subgraph2") { + { + ... on T1 { + __typename + id + } + ... on T2 { + __typename + id + } + } => + { + ... on T1 { + a + } + ... on T2 { + b + } + } + }, + }, + }, + } + "### + ); + + // Ensuring that querying only `a` can be done with subgraph1 only when provided. + assert_plan!( + &planner, + r#" + { + withProvidesForT1 { + ... on T1 { + a + } + ... on T2 { + a + } + } + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + withProvidesForT1 { + __typename + ... on T1 { + a + } + ... on T2 { + a + } + } + } + }, + } + "### + ); + + // But ensure that querying `b` still goes to subgraph2 if only a is provided. + assert_plan!( + &planner, + r#" + { + withProvidesForT1 { + ... on T1 { + a + } + ... on T2 { + a + b + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + withProvidesForT1 { + __typename + ... on T1 { + a + } + ... on T2 { + __typename + id + a + } + } + } + }, + Flatten(path: "withProvidesForT1") { + Fetch(service: "Subgraph2") { + { + ... on T2 { + __typename + id + } + } => + { + ... on T2 { + b + } + } + }, + }, + }, + } + "### + ); + + // Lastly, if both are provided, ensures we only hit subgraph1. + assert_plan!( + &planner, + r#" + { + withProvidesForBoth { + ... on T1 { + a + } + ... on T2 { + a + b + } + } + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + withProvidesForBoth { + __typename + ... on T1 { + a + } + ... on T2 { + a + b + } + } + } + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "Schema has no type \"I\"")] +// TODO: investigate this failure +fn it_allow_providing_fields_for_only_some_subtype() { + let planner = planner!( + Subgraph1: r#" + type Query { + noProvides: I + withProvidesOnA: I @provides(fields: "... on T2 { a }") + withProvidesOnB: I @provides(fields: "... on T2 { b }") + } + + interface I { + a: Int + b: Int + } + + type T1 implements I @key(fields: "id") { + id: ID! + a: Int + b: Int @external + } + + type T2 implements I @key(fields: "id") { + id: ID! + a: Int @external + b: Int @external + } + "#, + Subgraph2: r#" + type T1 @key(fields: "id") { + id: ID! + b: Int + } + + type T2 @key(fields: "id") { + id: ID! + a: Int @shareable + b: Int @shareable + } + "#, + ); + assert_plan!( + &planner, + r#" + { + noProvides { + a + b + } + } + "#, + + + // This is our sanity check: we first query _without_ the provides + // to make sure we _do_ need to go the the second subgraph. + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + noProvides { + __typename + ... on T1 { + __typename + id + a + } + ... on T2 { + __typename + id + } + } + } + }, + Flatten(path: "noProvides") { + Fetch(service: "Subgraph2") { + { + ... on T1 { + __typename + id + } + ... on T2 { + __typename + id + } + } => + { + ... on T1 { + b + } + ... on T2 { + a + b + } + } + }, + }, + }, + } + "### + ); + + // Ensuring that querying only `a` can be done with subgraph1 only. + assert_plan!( + &planner, + r#" + { + withProvidesOnA { + a + } + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + withProvidesOnA { + __typename + ... on T1 { + a + } + ... on T2 { + a + } + } + } + }, + } + "### + ); + + // Ensuring that for `b`, only the T2 value is provided by subgraph1. + assert_plan!( + &planner, + r#" + { + withProvidesOnB { + b + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + withProvidesOnB { + __typename + ... on T1 { + __typename + id + } + ... on T2 { + b + } + } + } + }, + Flatten(path: "withProvidesOnB") { + Fetch(service: "Subgraph2") { + { + ... on T1 { + __typename + id + } + } => + { + ... on T1 { + b + } + } + }, + }, + }, + } + "### + ); + + // But if we only query for T2, then no reason to go to subgraph2. + assert_plan!( + &planner, + r#" + { + withProvidesOnB { + ... on T2 { + b + } + } + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + withProvidesOnB { + __typename + ... on T2 { + b + } + } + } + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "Subgraph unexpectedly does not use federation spec")] +// TODO: investigate this failure +fn it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides() { + let planner = planner!( + Subgraph1: r#" + type Query { + noProvides: E + withProvides: E @provides(fields: "i { a ... on T1 { b } }") + } + + type E @key(fields: "id") { + id: ID! + i: I @external + } + + interface I { + a: Int + } + + type T1 implements I @key(fields: "id") { + id: ID! + a: Int @external + b: Int @external + } + + type T2 implements I @key(fields: "id") { + id: ID! + a: Int @external + } + "#, + Subgraph2: r#" + type E @key(fields: "id") { + id: ID! + i: I @shareable + } + + interface I { + a: Int + } + + type T1 implements I @key(fields: "id") { + id: ID! + a: Int @shareable + b: Int @shareable + } + + type T2 implements I @key(fields: "id") { + id: ID! + a: Int @shareable + c: Int + } + "#, + ); + assert_plan!( + &planner, + r#" + { + noProvides { + i { + a + ... on T1 { + b + } + ... on T2 { + c + } + } + } + } + "#, + + + // This is our sanity check: we first query _without_ the provides to make sure we _do_ need to + // go the the second subgraph for everything. + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + noProvides { + __typename + id + } + } + }, + Flatten(path: "noProvides") { + Fetch(service: "Subgraph2") { + { + ... on E { + __typename + id + } + } => + { + ... on E { + i { + __typename + a + ... on T1 { + b + } + ... on T2 { + c + } + } + } + } + }, + }, + }, + } + "### + ); + + // But the same operation with the provides allow to get what is provided from the first subgraph. + assert_plan!( + &planner, + r#" + { + withProvides { + i { + a + ... on T1 { + b + } + ... on T2 { + c + } + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + withProvides { + i { + __typename + a + ... on T1 { + b + } + ... on T2 { + __typename + id + } + } + } + } + }, + Flatten(path: "withProvides.i") { + Fetch(service: "Subgraph2") { + { + ... on T2 { + __typename + id + } + } => + { + ... on T2 { + c + } + } + }, + }, + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/requires.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/requires.rs new file mode 100644 index 0000000000..6596331eeb --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/requires.rs @@ -0,0 +1,1406 @@ +mod include_skip; + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn it_handles_multiple_requires_within_the_same_entity_fetch() { + let planner = planner!( + Subgraph1: r#" + type Query { + is: [I!]! + } + + interface I { + id: ID! + f: Int + g: Int + } + + type T1 implements I { + id: ID! + f: Int + g: Int + } + + type T2 implements I @key(fields: "id") { + id: ID! + f: Int! + g: Int @external + } + + type T3 implements I @key(fields: "id") { + id: ID! + f: Int + g: Int @external + } + "#, + Subgraph2: r#" + type T2 @key(fields: "id") { + id: ID! + f: Int! @external + g: Int @requires(fields: "f") + } + + type T3 @key(fields: "id") { + id: ID! + f: Int @external + g: Int @requires(fields: "f") + } + "#, + ); + assert_plan!( + &planner, + r#" + { + is { + g + } + } + "#, + + + // The main goal of this test is to show that the 2 @requires for `f` gets handled seemlessly + // into the same fetch group. But note that because the type for `f` differs, the 2nd instance + // gets aliased (or the fetch would be invalid graphQL). + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + is { + __typename + ... on T1 { + g + } + ... on T2 { + __typename + id + f + } + ... on T3 { + __typename + id + f__alias_0: f + } + } + } + }, + Flatten(path: "is.@") { + Fetch(service: "Subgraph2") { + { + ... on T2 { + __typename + id + f + } + ... on T3 { + __typename + id + f + } + } => + { + ... on T2 { + g + } + ... on T3 { + g + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn handles_multiple_requires_involving_different_nestedness() { + let planner = planner!( + Subgraph1: r#" + type Query { + list: [Item] + } + + type Item @key(fields: "user { id }") { + id: ID! + value: String + user: User + } + + type User @key(fields: "id") { + id: ID! + value: String + } + "#, + Subgraph2: r#" + type Item @key(fields: "user { id }") { + user: User + value: String @external + computed: String @requires(fields: "user { value } value") + computed2: String @requires(fields: "user { value }") + } + + type User @key(fields: "id") { + id: ID! + value: String @external + computed: String @requires(fields: "value") + } + "#, + ); + assert_plan!( + &planner, + r#" + { + list { + computed + computed2 + user { + computed + } + } + } + "#, + + + // The main goal of this test is to show that the 2 @requires for `f` gets handled seemlessly + // into the same fetch group. + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + list { + __typename + user { + __typename + id + value + } + value + } + } + }, + Parallel { + Flatten(path: "list.@") { + Fetch(service: "Subgraph2") { + { + ... on Item { + __typename + user { + id + value + } + value + } + } => + { + ... on Item { + computed + computed2 + } + } + }, + }, + Flatten(path: "list.@.user") { + Fetch(service: "Subgraph2") { + { + ... on User { + __typename + id + value + } + } => + { + ... on User { + computed + } + } + }, + }, + }, + }, + } + "### + ); +} + +/// require that depends on another require +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn it_handles_simple_require_chain() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + v: Int! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v: Int! @external + inner: Int! @requires(fields: "v") + } + "#, + Subgraph3: r#" + type T @key(fields: "id") { + id: ID! + inner: Int! @external + outer: Int! @requires(fields: "inner") + } + "# + ); + // Ensures that if we only ask `outer`, we get everything needed in between. + assert_plan!( + &planner, + r#" + { + t { + outer + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + v + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + v + id + } + } => + { + ... on T { + inner + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph3") { + { + ... on T { + __typename + inner + id + } + } => + { + ... on T { + outer + } + } + }, + }, + }, + } + "### + ); + + // Ensures that manually asking for the required dependencies doesn't change anything + // (note: technically it happens to switch the order of fields in the inputs of "Subgraph2" + // so the plans are not 100% the same "string", which is why we inline it in both cases, + // but that's still the same plan and a perfectly valid output). + assert_plan!( + &planner, + r#" + { + t { + v + inner + outer + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + v + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + v + } + } => + { + ... on T { + inner + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph3") { + { + ... on T { + __typename + inner + id + } + } => + { + ... on T { + outer + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn it_handles_require_chain_not_ending_in_original_group() { + // This is somewhat simiar to the 'simple require chain' case, but the chain does not + // end in the group in which the query start + 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! + v: Int! @external + inner: Int! @requires(fields: "v") + } + "#, + Subgraph3: r#" + type T @key(fields: "id") { + id: ID! + inner: Int! @external + outer: Int! @requires(fields: "inner") + } + "#, + Subgraph4: r#" + type T @key(fields: "id") { + id: ID! + v: Int! + } + "#, + ); + // Ensures that if we only ask `outer`, we get everything needed in between. + assert_plan!( + &planner, + r#" + { + t { + outer + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph4") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + v + id + } + } => + { + ... on T { + inner + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph3") { + { + ... on T { + __typename + inner + id + } + } => + { + ... on T { + outer + } + } + }, + }, + }, + } + "### + ); + + // Ensures that manually asking for the required dependencies doesn't change anything. + assert_plan!( + &planner, + r#" + { + t { + v + inner + outer + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph4") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + v + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + v + id + } + } => + { + ... on T { + inner + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph3") { + { + ... on T { + __typename + inner + id + } + } => + { + ... on T { + outer + } + } + }, + }, + }, + } + "### + ); +} + +/// a chain of 10 requires +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn it_handles_longer_require_chain() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + v1: Int! + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + v1: Int! @external + v2: Int! @requires(fields: "v1") + } + "#, + Subgraph3: r#" + type T @key(fields: "id") { + id: ID! + v2: Int! @external + v3: Int! @requires(fields: "v2") + } + "#, + Subgraph4: r#" + type T @key(fields: "id") { + id: ID! + v3: Int! @external + v4: Int! @requires(fields: "v3") + } + "#, + Subgraph5: r#" + type T @key(fields: "id") { + id: ID! + v4: Int! @external + v5: Int! @requires(fields: "v4") + } + "#, + Subgraph6: r#" + type T @key(fields: "id") { + id: ID! + v5: Int! @external + v6: Int! @requires(fields: "v5") + } + "#, + Subgraph7: r#" + type T @key(fields: "id") { + id: ID! + v6: Int! @external + v7: Int! @requires(fields: "v6") + } + "#, + Subgraph8: r#" + type T @key(fields: "id") { + id: ID! + v7: Int! @external + v8: Int! @requires(fields: "v7") + } + "#, + Subgraph9: r#" + type T @key(fields: "id") { + id: ID! + v8: Int! @external + v9: Int! @requires(fields: "v8") + } + "#, + Subgraph10: r#" + type T @key(fields: "id") { + id: ID! + v9: Int! @external + v10: Int! @requires(fields: "v9") + } + "#, + ); + // Ensures that if we only ask `outer`, we get everything needed in between. + assert_plan!( + &planner, + r#" + { + t { + v10 + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + v1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + v1 + id + } + } => + { + ... on T { + v2 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph3") { + { + ... on T { + __typename + v2 + id + } + } => + { + ... on T { + v3 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph4") { + { + ... on T { + __typename + v3 + id + } + } => + { + ... on T { + v4 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph5") { + { + ... on T { + __typename + v4 + id + } + } => + { + ... on T { + v5 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph6") { + { + ... on T { + __typename + v5 + id + } + } => + { + ... on T { + v6 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph7") { + { + ... on T { + __typename + v6 + id + } + } => + { + ... on T { + v7 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph8") { + { + ... on T { + __typename + v7 + id + } + } => + { + ... on T { + v8 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph9") { + { + ... on T { + __typename + v8 + id + } + } => + { + ... on T { + v9 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph10") { + { + ... on T { + __typename + v9 + id + } + } => + { + ... on T { + v10 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn it_handles_complex_require_chain() { + // Another "require chain" test but with more complexity as we have a require on multiple fields, some of which being + // nested, and having requirements of their own. + 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! + inner1: Int! + inner2_required: Int! + } + "#, + Subgraph3: r#" + type T @key(fields: "id") { + id: ID! + inner2_required: Int! @external + inner2: Int! @requires(fields: "inner2_required") + } + "#, + Subgraph4: r#" + type T @key(fields: "id") { + id: ID! + inner3: Inner3Type! + } + + type Inner3Type @key(fields: "k3") { + k3: ID! + } + + type Inner4Type @key(fields: "k4") { + k4: ID! + inner4_required: Int! + } + "#, + Subgraph5: r#" + type T @key(fields: "id") { + id: ID! + inner1: Int! @external + inner2: Int! @external + inner3: Inner3Type! @external + inner4: Inner4Type! @external + inner5: Int! @external + outer: Int! + @requires( + fields: "inner1 inner2 inner3 { inner3_nested } inner4 { inner4_nested } inner5" + ) + } + + type Inner3Type @key(fields: "k3") { + k3: ID! + inner3_nested: Int! + } + + type Inner4Type @key(fields: "k4") { + k4: ID! + inner4_nested: Int! @requires(fields: "inner4_required") + inner4_required: Int! @external + } + "#, + Subgraph6: r#" + type T @key(fields: "id") { + id: ID! + inner4: Inner4Type! + } + + type Inner4Type @key(fields: "k4") { + k4: ID! + } + "#, + Subgraph7: r#" + type T @key(fields: "id") { + id: ID! + inner5: Int! + } + "# + ); + + assert_plan!( + &planner, + r#" + { + t { + outer + } + } + "#, + + + // This is a big plan, but afaict, this is optimal. That is, there is 3 main steps: + // 1. it get the `id` for `T`, which is needed for anything else. + // 2. it gets all the dependencies of for the @require on `outer` in parallel + // 3. it finally get `outer`, passing all requirements as inputs. + // + // The 2nd step is the most involved, but it's just gathering the "outer" requirements in parallel, + // while satisfying the "inner" requirements in each branch. + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + } + } + }, + Parallel { + Sequence { + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + inner2_required + inner1 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph3") { + { + ... on T { + __typename + inner2_required + id + } + } => + { + ... on T { + inner2 + } + } + }, + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph7") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + inner5 + } + } + }, + }, + Sequence { + Flatten(path: "t") { + Fetch(service: "Subgraph6") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + inner4 { + __typename + k4 + } + } + } + }, + }, + Flatten(path: "t.inner4") { + Fetch(service: "Subgraph4") { + { + ... on Inner4Type { + __typename + k4 + } + } => + { + ... on Inner4Type { + inner4_required + } + } + }, + }, + Flatten(path: "t.inner4") { + Fetch(service: "Subgraph5") { + { + ... on Inner4Type { + __typename + inner4_required + k4 + } + } => + { + ... on Inner4Type { + inner4_nested + } + } + }, + }, + }, + Sequence { + Flatten(path: "t") { + Fetch(service: "Subgraph4") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + inner3 { + __typename + k3 + } + } + } + }, + }, + Flatten(path: "t.inner3") { + Fetch(service: "Subgraph5") { + { + ... on Inner3Type { + __typename + k3 + } + } => + { + ... on Inner3Type { + inner3_nested + } + } + }, + }, + }, + }, + Flatten(path: "t") { + Fetch(service: "Subgraph5") { + { + ... on T { + __typename + inner1 + inner2 + inner3 { + inner3_nested + } + inner4 { + inner4_nested + } + inner5 + id + } + } => + { + ... on T { + outer + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn it_handes_diamond_shape_depedencies() { + // The idea of this test is that to be able to fulfill the @require in subgraph D, we need + // both values from C for the @require and values from B for the key itself, but both + // B and C can be queried directly after the initial query to A. This make the optimal query + // plan diamond-shaped: after starting in A, we can get everything from B and C in + // parallel, and then D needs to wait on both of those to run. + + let planner = planner!( + A: r#" + type Query { + t: T + } + + type T @key(fields: "id1") { + id1: ID! + } + "#, + B: r#" + type T @key(fields: "id1") @key(fields: "id2") { + id1: ID! + id2: ID! + v1: Int + v2: Int + } + "#, + C: r#" + type T @key(fields: "id1") { + id1: ID! + v3: Int + } + "#, + D: r#" + type T @key(fields: "id2") { + id2: ID! + v3: Int @external + v4: Int @requires(fields: "v3") + } + "# + ); + assert_plan!( + &planner, + r#" + { + t { + v1 + v2 + v3 + v4 + } + } + "#, + + + // The optimal plan should: + // 1. fetch id1 from A + // 2. from that, it can both (in parallel): + // - get id2, v1 and v2 from B + // - get v3 from C + // 3. lastly, once both of those return, it can get v4 from D as it has all requirement + @r###" + QueryPlan { + Sequence { + Fetch(service: "A") { + { + t { + __typename + id1 + } + } + }, + Parallel { + Flatten(path: "t") { + Fetch(service: "B") { + { + ... on T { + __typename + id1 + } + } => + { + ... on T { + __typename + id2 + v1 + v2 + id1 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "C") { + { + ... on T { + __typename + id1 + } + } => + { + ... on T { + v3 + } + } + }, + }, + }, + Flatten(path: "t") { + Fetch(service: "D") { + { + ... on T { + __typename + v3 + id2 + } + } => + { + ... on T { + v4 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn it_can_require_at_inaccessible_fields() { + let planner = planner!( + Subgraph1: r#" + type Query { + one: One + onlyIn1: Int + } + + type One @key(fields: "id") { + id: ID! + a: String @inaccessible + onlyIn1: Int + } + "#, + Subgraph2: r#" + type Query { + onlyIn2: Int + } + + type One @key(fields: "id") { + id: ID! + a: String @external + b: String @requires(fields: "a") + onlyIn2: Int + } + "#, + ); + assert_plan!( + &planner, + r#" + { + one { + b + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + one { + __typename + id + a + } + } + }, + Flatten(path: "one") { + Fetch(service: "Subgraph2") { + { + ... on One { + __typename + id + a + } + } => + { + ... on One { + b + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another() { + // The specificity of this example is that we `T.v` requires 2 fields `req1` + // and `req2`, but `req1` is also a key to get `req2`. This dependency was + // confusing a previous version of the code (which, when gathering the + // "createdGroups" for `T.v` @requires, was using the group for `req1` twice + // separatly (instead of recognizing it was the same group), and this was + // confusing the rest of the code was wasn't expecting it. + let planner = planner!( + A: r#" + type Query { + t: T + } + + type T @key(fields: "id1") @key(fields: "req1") { + id1: ID! + req1: Int + } + "#, + B: r#" + type T @key(fields: "id1") { + id1: ID! + req1: Int @external + req2: Int @external + v: Int @requires(fields: "req1 req2") + } + "#, + C: r#" + type T @key(fields: "req1") { + req1: Int + req2: Int + } + "# + ); + + assert_plan!( + &planner, + r#" + { + t { + v + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "A") { + { + t { + __typename + id1 + req1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "C") { + { + ... on T { + __typename + req1 + } + } => + { + ... on T { + req2 + } + } + }, + }, + Flatten(path: "t") { + Fetch(service: "B") { + { + ... on T { + __typename + req1 + req2 + id1 + } + } => + { + ... on T { + v + } + } + }, + }, + }, + } + "### + ); +} 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 new file mode 100644 index 0000000000..e63f50e9d4 --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/requires/include_skip.rs @@ -0,0 +1,244 @@ +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn it_handles_a_simple_at_requires_triggered_within_a_conditional() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + a: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + a: Int @external + b: Int @requires(fields: "a") + } + "#, + ); + assert_plan!( + &planner, + r#" + query foo($test: Boolean!) { + t @include(if: $test) { + b + } + } + "#, + @r###" + QueryPlan { + Include(if: $test) { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + a + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + a + } + } => + { + ... on T { + b + } + } + }, + }, + } + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +fn it_handles_an_at_requires_triggered_conditionally() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + a: Int + } + "#, + Subgraph2: r#" + type T @key(fields: "id") { + id: ID! + a: Int @external + b: Int @requires(fields: "a") + } + "#, + ); + assert_plan!( + &planner, + r#" + query foo($test: Boolean!) { + t { + b @include(if: $test) + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + ... on T @include(if: $test) { + a + } + } + } + }, + Include(if: $test) { + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + a + } + } => + { + ... on T { + b + } + } + }, + } + }, + }, + } + "### + ); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +// TODO: investigate this failure +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! + } + "#, + Subgraph2: r#" + type A @key(fields: "idA") { + idA: ID! + b: [B] + } + + type B @key(fields: "idB") { + idB: ID! + required: Int + } + "#, + Subgraph3: r#" + type B @key(fields: "idB") { + idB: ID! + c: Int @requires(fields: "required") + required: Int @external + } + "#, + ); + + assert_plan!( + &planner, + r#" + query foo($test1: Boolean!, $test2: Boolean!) { + a @include(if: $test1) { + b @include(if: $test2) { + c + } + } + } + "#, + @r###" + QueryPlan { + Include(if: $test1) { + Sequence { + Fetch(service: "Subgraph1") { + { + a { + __typename + idA + } + } + }, + Include(if: $test2) { + Sequence { + Flatten(path: "a") { + Fetch(service: "Subgraph2") { + { + ... on A { + __typename + idA + } + } => + { + ... on A { + b { + __typename + idB + required + } + } + } + }, + }, + Flatten(path: "a.b.@") { + Fetch(service: "Subgraph3") { + { + ... on B { + ... on B { + __typename + idB + required + } + } + } => + { + ... on B { + ... on B { + c + } + } + } + }, + }, + } + }, + } + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql b/apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql new file mode 100644 index 0000000000..cfa0ce48ca --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql @@ -0,0 +1,70 @@ +# Composed from subgraphs with hash: 569b27fb405f035347590f869510e5cc78058214 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T +} + +type T + @join__type(graph: S1) + @join__type(graph: S2) +{ + f0: Int + f1: Int + f2: Int + f3: Int + f4: Int + f5: Int + f6: Int + f7: Int + f8: Int + f9: Int + f10: Int + f11: Int + f12: Int + f13: Int + f14: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql b/apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql new file mode 100644 index 0000000000..ab27753e5d --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql @@ -0,0 +1,57 @@ +# Composed from subgraphs with hash: 39d8b2af4d4c017bda2b76a66d2128757ae8ac5d +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + A_NA_ME_WITH_PLEN_TY_REPLACE_MENTS @join__graph(name: "a-na&me-with-plen&ty-replace*ments", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: A_NA_ME_WITH_PLEN_TY_REPLACE_MENTS) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: A_NA_ME_WITH_PLEN_TY_REPLACE_MENTS, key: "id") +{ + id: ID! + x: Int @join__field(graph: A_NA_ME_WITH_PLEN_TY_REPLACE_MENTS) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql b/apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql new file mode 100644 index 0000000000..c107e04990 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql @@ -0,0 +1,65 @@ +# Composed from subgraphs with hash: cea83739e9f98b70a1185463f8b9c3c17f2064c8 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +interface Interface + @join__type(graph: SUBGRAPH1) +{ + field: Interface +} + +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 Object implements Interface + @join__implements(graph: SUBGRAPH1, interface: "Interface") + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + field: Object @join__field(graph: SUBGRAPH1, provides: "x") + x: Int @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2) +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + dummy: Interface @join__field(graph: SUBGRAPH1) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql b/apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql new file mode 100644 index 0000000000..f43bbbed3d --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql @@ -0,0 +1,57 @@ +# Composed from subgraphs with hash: 6655be753e0c804f83461f143965a01f3a040e1d +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + NON_GRAPHQL_NAME @join__graph(name: "non-graphql-name", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: NON_GRAPHQL_NAME) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: NON_GRAPHQL_NAME, key: "id") +{ + id: ID! + x: Int @join__field(graph: NON_GRAPHQL_NAME) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql b/apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql new file mode 100644 index 0000000000..1e9922d407 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql @@ -0,0 +1,57 @@ +# Composed from subgraphs with hash: 3d0f2fed8651dd6a1c0d4b7bb3c94989781c8b68 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + _42_ @join__graph(name: "42!", url: "none") + S1 @join__graph(name: "S1", 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: _42_) + @join__type(graph: S1) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: _42_, key: "id") + @join__type(graph: S1, key: "id") +{ + id: ID! + x: Int @join__field(graph: _42_) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql new file mode 100644 index 0000000000..78d523b512 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql @@ -0,0 +1,69 @@ +# Composed from subgraphs with hash: de3bbad205ccca07c2a0fe699d358368241993b7 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type Item + @join__type(graph: SUBGRAPH1, key: "user { id }") + @join__type(graph: SUBGRAPH2, key: "user { id }") +{ + id: ID! @join__field(graph: SUBGRAPH1) + value: String @join__field(graph: SUBGRAPH1) @join__field(graph: SUBGRAPH2, external: true) + user: User + computed: String @join__field(graph: SUBGRAPH2, requires: "user { value } value") + computed2: String @join__field(graph: SUBGRAPH2, requires: "user { value }") +} + +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) +{ + list: [Item] @join__field(graph: SUBGRAPH1) +} + +type User + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + value: String @join__field(graph: SUBGRAPH1) @join__field(graph: SUBGRAPH2, external: true) + computed: String @join__field(graph: SUBGRAPH2, requires: "value") +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql b/apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql new file mode 100644 index 0000000000..9cb1ecc9b4 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql @@ -0,0 +1,78 @@ +# Composed from subgraphs with hash: 84c56a8f07d5763e8dfa886575ed2a5df80bbaf6 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +interface I + @join__type(graph: SUBGRAPH1) +{ + a: Int + b: Int +} + +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) +{ + noProvides: I @join__field(graph: SUBGRAPH1) + withProvidesOnA: I @join__field(graph: SUBGRAPH1, provides: "... on T2 { a }") + withProvidesOnB: I @join__field(graph: SUBGRAPH1, provides: "... on T2 { b }") +} + +type T1 implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @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, external: true) @join__field(graph: SUBGRAPH2) +} + +type T2 implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + a: Int @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2) + b: Int @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql new file mode 100644 index 0000000000..a4bcc63125 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql @@ -0,0 +1,65 @@ +# Composed from subgraphs with hash: 78f8e2391131a01115d7025d6f7aae3763f91d5e +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) +{ + query: Query +} + +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + 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 One + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + a: String @inaccessible @join__field(graph: SUBGRAPH1) @join__field(graph: SUBGRAPH2, external: true) + onlyIn1: Int @join__field(graph: SUBGRAPH1) + b: String @join__field(graph: SUBGRAPH2, requires: "a") + onlyIn2: Int @join__field(graph: SUBGRAPH2) +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + one: One @join__field(graph: SUBGRAPH1) + onlyIn1: Int @join__field(graph: SUBGRAPH1) + onlyIn2: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql new file mode 100644 index 0000000000..1ef09945bd --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql @@ -0,0 +1,68 @@ +# Composed from subgraphs with hash: c6a3588d5bfe556f3de3cf2f3fa2165704f9206d +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + A @join__graph(name: "A", url: "none") + B @join__graph(name: "B", url: "none") + C @join__graph(name: "C", url: "none") + D @join__graph(name: "D", 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: A) + @join__type(graph: B) + @join__type(graph: C) + @join__type(graph: D) +{ + t: T @join__field(graph: A) +} + +type T + @join__type(graph: A, key: "id1") + @join__type(graph: B, key: "id1") + @join__type(graph: B, key: "id2") + @join__type(graph: C, key: "id1") + @join__type(graph: D, key: "id2") +{ + id1: ID! @join__field(graph: A) @join__field(graph: B) @join__field(graph: C) + id2: ID! @join__field(graph: B) @join__field(graph: D) + v1: Int @join__field(graph: B) + v2: Int @join__field(graph: B) + v3: Int @join__field(graph: C) @join__field(graph: D, external: true) + v4: Int @join__field(graph: D, requires: "v3") +} 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 new file mode 100644 index 0000000000..60ae71d098 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql @@ -0,0 +1,58 @@ +# Composed from subgraphs with hash: d6db359dab5222fd4d4da957d4ece13e5dee392f +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + 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) @join__field(graph: SUBGRAPH2, external: true) + b: Int @join__field(graph: SUBGRAPH2, requires: "a") +} 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 new file mode 100644 index 0000000000..60ae71d098 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql @@ -0,0 +1,58 @@ +# Composed from subgraphs with hash: d6db359dab5222fd4d4da957d4ece13e5dee392f +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + 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) @join__field(graph: SUBGRAPH2, external: true) + b: Int @join__field(graph: SUBGRAPH2, requires: "a") +} 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 new file mode 100644 index 0000000000..0760e3ad79 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql @@ -0,0 +1,68 @@ +# Composed from subgraphs with hash: cc5702c4822eb337c8ae95605bcb51812750cff8 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type A + @join__type(graph: SUBGRAPH1, key: "idA") + @join__type(graph: SUBGRAPH2, key: "idA") +{ + idA: ID! + b: [B] @join__field(graph: SUBGRAPH2) +} + +type B + @join__type(graph: SUBGRAPH2, key: "idB") + @join__type(graph: SUBGRAPH3, key: "idB") +{ + idB: ID! + required: Int @join__field(graph: SUBGRAPH2) @join__field(graph: SUBGRAPH3, external: true) + c: Int @join__field(graph: SUBGRAPH3, requires: "required") +} + +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) +{ + a: A @join__field(graph: SUBGRAPH1) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql new file mode 100644 index 0000000000..8a382fa437 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql @@ -0,0 +1,96 @@ +# Composed from subgraphs with hash: 722cd56dba6c8500f961ba2c48cc9f643b2fda98 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type Inner3Type + @join__type(graph: SUBGRAPH4, key: "k3") + @join__type(graph: SUBGRAPH5, key: "k3") +{ + k3: ID! + inner3_nested: Int! @join__field(graph: SUBGRAPH5) +} + +type Inner4Type + @join__type(graph: SUBGRAPH4, key: "k4") + @join__type(graph: SUBGRAPH5, key: "k4") + @join__type(graph: SUBGRAPH6, key: "k4") +{ + k4: ID! + inner4_required: Int! @join__field(graph: SUBGRAPH4) @join__field(graph: SUBGRAPH5, external: true) + inner4_nested: Int! @join__field(graph: SUBGRAPH5, requires: "inner4_required") +} + +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") + SUBGRAPH5 @join__graph(name: "Subgraph5", url: "none") + SUBGRAPH6 @join__graph(name: "Subgraph6", url: "none") + SUBGRAPH7 @join__graph(name: "Subgraph7", 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) + @join__type(graph: SUBGRAPH5) + @join__type(graph: SUBGRAPH6) + @join__type(graph: SUBGRAPH7) +{ + 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") + @join__type(graph: SUBGRAPH4, key: "id") + @join__type(graph: SUBGRAPH5, key: "id") + @join__type(graph: SUBGRAPH6, key: "id") + @join__type(graph: SUBGRAPH7, key: "id") +{ + id: ID! + inner1: Int! @join__field(graph: SUBGRAPH2) @join__field(graph: SUBGRAPH5, external: true) + inner2_required: Int! @join__field(graph: SUBGRAPH2) @join__field(graph: SUBGRAPH3, external: true) + inner2: Int! @join__field(graph: SUBGRAPH3, requires: "inner2_required") @join__field(graph: SUBGRAPH5, external: true) + inner3: Inner3Type! @join__field(graph: SUBGRAPH4) @join__field(graph: SUBGRAPH5, external: true) + inner4: Inner4Type! @join__field(graph: SUBGRAPH5, external: true) @join__field(graph: SUBGRAPH6) + inner5: Int! @join__field(graph: SUBGRAPH5, external: true) @join__field(graph: SUBGRAPH7) + outer: Int! @join__field(graph: SUBGRAPH5, requires: "inner1 inner2 inner3 { inner3_nested } inner4 { inner4_nested } inner5") +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql new file mode 100644 index 0000000000..207d9de6f3 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql @@ -0,0 +1,90 @@ +# Composed from subgraphs with hash: 125a036421e2f6b9aa7e2c1bcabc29a4e3aa32cf +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") + SUBGRAPH10 @join__graph(name: "Subgraph10", url: "none") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") + SUBGRAPH3 @join__graph(name: "Subgraph3", url: "none") + SUBGRAPH4 @join__graph(name: "Subgraph4", url: "none") + SUBGRAPH5 @join__graph(name: "Subgraph5", url: "none") + SUBGRAPH6 @join__graph(name: "Subgraph6", url: "none") + SUBGRAPH7 @join__graph(name: "Subgraph7", url: "none") + SUBGRAPH8 @join__graph(name: "Subgraph8", url: "none") + SUBGRAPH9 @join__graph(name: "Subgraph9", 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: SUBGRAPH10) + @join__type(graph: SUBGRAPH2) + @join__type(graph: SUBGRAPH3) + @join__type(graph: SUBGRAPH4) + @join__type(graph: SUBGRAPH5) + @join__type(graph: SUBGRAPH6) + @join__type(graph: SUBGRAPH7) + @join__type(graph: SUBGRAPH8) + @join__type(graph: SUBGRAPH9) +{ + t: T @join__field(graph: SUBGRAPH1) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH10, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") + @join__type(graph: SUBGRAPH3, key: "id") + @join__type(graph: SUBGRAPH4, key: "id") + @join__type(graph: SUBGRAPH5, key: "id") + @join__type(graph: SUBGRAPH6, key: "id") + @join__type(graph: SUBGRAPH7, key: "id") + @join__type(graph: SUBGRAPH8, key: "id") + @join__type(graph: SUBGRAPH9, key: "id") +{ + id: ID! + v1: Int! @join__field(graph: SUBGRAPH1) @join__field(graph: SUBGRAPH2, external: true) + v9: Int! @join__field(graph: SUBGRAPH10, external: true) @join__field(graph: SUBGRAPH9, requires: "v8") + v10: Int! @join__field(graph: SUBGRAPH10, requires: "v9") + v2: Int! @join__field(graph: SUBGRAPH2, requires: "v1") @join__field(graph: SUBGRAPH3, external: true) + v3: Int! @join__field(graph: SUBGRAPH3, requires: "v2") @join__field(graph: SUBGRAPH4, external: true) + v4: Int! @join__field(graph: SUBGRAPH4, requires: "v3") @join__field(graph: SUBGRAPH5, external: true) + v5: Int! @join__field(graph: SUBGRAPH5, requires: "v4") @join__field(graph: SUBGRAPH6, external: true) + v6: Int! @join__field(graph: SUBGRAPH6, requires: "v5") @join__field(graph: SUBGRAPH7, external: true) + v7: Int! @join__field(graph: SUBGRAPH7, requires: "v6") @join__field(graph: SUBGRAPH8, external: true) + v8: Int! @join__field(graph: SUBGRAPH8, requires: "v7") @join__field(graph: SUBGRAPH9, external: true) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql new file mode 100644 index 0000000000..e598d84a1b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql @@ -0,0 +1,86 @@ +# Composed from subgraphs with hash: dd69cd7ccb9070ab8a3de08ead3e41eacce180d1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +interface I + @join__type(graph: SUBGRAPH1) +{ + id: ID! + f: Int + g: Int +} + +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) +{ + is: [I!]! @join__field(graph: SUBGRAPH1) +} + +type T1 implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__type(graph: SUBGRAPH1) +{ + id: ID! + f: Int + g: Int +} + +type T2 implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + f: Int! @join__field(graph: SUBGRAPH1) @join__field(graph: SUBGRAPH2, external: true) + g: Int @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2, requires: "f") +} + +type T3 implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + f: Int @join__field(graph: SUBGRAPH1) @join__field(graph: SUBGRAPH2, external: true) + g: Int @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2, requires: "f") +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql new file mode 100644 index 0000000000..251390eef3 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql @@ -0,0 +1,65 @@ +# Composed from subgraphs with hash: fadb16531ae6d8fe34df35cd8993071bd83b8160 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + 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: "id") + @join__type(graph: SUBGRAPH2, key: "id") + @join__type(graph: SUBGRAPH3, key: "id") + @join__type(graph: SUBGRAPH4, key: "id") +{ + id: ID! + v: Int! @join__field(graph: SUBGRAPH2, external: true) @join__field(graph: SUBGRAPH4) + inner: Int! @join__field(graph: SUBGRAPH2, requires: "v") @join__field(graph: SUBGRAPH3, external: true) + outer: Int! @join__field(graph: SUBGRAPH3, requires: "inner") +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql new file mode 100644 index 0000000000..15556cf8e9 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql @@ -0,0 +1,62 @@ +# Composed from subgraphs with hash: 49a33e65faa01be4bcd2f444dd5f10f439a286a8 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + 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! + v: Int! @join__field(graph: SUBGRAPH1) @join__field(graph: SUBGRAPH2, external: true) + inner: Int! @join__field(graph: SUBGRAPH2, requires: "v") @join__field(graph: SUBGRAPH3, external: true) + outer: Int! @join__field(graph: SUBGRAPH3, requires: "inner") +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql b/apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql new file mode 100644 index 0000000000..44f27a7576 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql @@ -0,0 +1,63 @@ +# Composed from subgraphs with hash: 57efb2956792ca0eb40a9df16cbd08aad77cfd44 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + A @join__graph(name: "A", url: "none") + B @join__graph(name: "B", url: "none") + C @join__graph(name: "C", 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: A) + @join__type(graph: B) + @join__type(graph: C) +{ + t: T @join__field(graph: A) +} + +type T + @join__type(graph: A, key: "id1") + @join__type(graph: A, key: "req1") + @join__type(graph: B, key: "id1") + @join__type(graph: C, key: "req1") +{ + id1: ID! @join__field(graph: A) @join__field(graph: B) + req1: Int @join__field(graph: A) @join__field(graph: B, external: true) @join__field(graph: C) + req2: Int @join__field(graph: B, external: true) @join__field(graph: C) + v: Int @join__field(graph: B, requires: "req1 req2") +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql new file mode 100644 index 0000000000..fabd270b4b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql @@ -0,0 +1,82 @@ +# Composed from subgraphs with hash: 8f950745a224d72bf86dc6d2c6a66c76ed7c5fe0 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +interface I + @join__type(graph: SUBGRAPH1) +{ + v: Value +} + +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) +{ + noProvides: I @join__field(graph: SUBGRAPH1) + withProvides: I @join__field(graph: SUBGRAPH1, provides: "v { a }") +} + +type T1 implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v: Value @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2) +} + +type T2 implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + v: Value @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2) +} + +type Value + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + a: Int + b: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql new file mode 100644 index 0000000000..c92e397384 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql @@ -0,0 +1,74 @@ +# Composed from subgraphs with hash: 627a6f4e651e7ff691b6c7d4104a68dd562dd7b7 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + 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) +{ + noProvides: U @join__field(graph: SUBGRAPH1) + withProvidesForT1: U @join__field(graph: SUBGRAPH1, provides: "... on T1 { a }") + withProvidesForBoth: U @join__field(graph: SUBGRAPH1, provides: "... on T1 { a } ... on T2 {b}") +} + +type T1 + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + a: Int @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2) +} + +type T2 + @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, external: true) @join__field(graph: SUBGRAPH2) +} + +union U + @join__type(graph: SUBGRAPH1) + @join__unionMember(graph: SUBGRAPH1, member: "T1") + @join__unionMember(graph: SUBGRAPH1, member: "T2") + = T1 | T2 diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql new file mode 100644 index 0000000000..bc8611b65a --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql @@ -0,0 +1,70 @@ +# Composed from subgraphs with hash: 952c14738ea71875811bacb2c3fdae24798ea16b +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + 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) +{ + doSomething: Response @join__field(graph: SUBGRAPH1) + doSomethingWithProvides: Response @join__field(graph: SUBGRAPH1, provides: "responseValue { subResponseValue { subSubResponseValue } }") +} + +type Response + @join__type(graph: SUBGRAPH1) +{ + responseValue: SubResponse +} + +type SubResponse + @join__type(graph: SUBGRAPH1) +{ + subResponseValue: SubSubResponse +} + +type SubSubResponse + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + subSubResponseValue: Int @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql new file mode 100644 index 0000000000..eb282becb9 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql @@ -0,0 +1,87 @@ +# Composed from subgraphs with hash: 3c2e16964e0f336b33ea0c717a1a45a4f11e6596 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type E + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + i: I @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2) +} + +interface I + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) +{ + a: Int +} + +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) +{ + noProvides: E @join__field(graph: SUBGRAPH1) + withProvides: E @join__field(graph: SUBGRAPH1, provides: "i { a ... on T1 { b } }") +} + +type T1 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, external: true) @join__field(graph: SUBGRAPH2) + b: Int @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2) +} + +type T2 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, external: true) @join__field(graph: SUBGRAPH2) + c: Int @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql b/apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql new file mode 100644 index 0000000000..74a64a0313 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: 87d7f4f15c5d3342bb4073f6926e35ba9d0cf435 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type Country + @join__type(graph: SUBGRAPH1, key: "iso") + @join__type(graph: SUBGRAPH2, key: "iso") +{ + iso: String! + currency: Currency! @join__field(graph: SUBGRAPH2) +} + +type Currency + @join__type(graph: SUBGRAPH2) +{ + name: String! + sign: String! +} + +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) +{ + transfers: [Transfer!]! @join__field(graph: SUBGRAPH1) +} + +type Transfer + @join__type(graph: SUBGRAPH1, key: "from { iso } to { iso }") + @join__type(graph: SUBGRAPH2, key: "from { iso } to { iso }") +{ + from: Country! + to: Country! + id: ID! @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 9d3b3bcec9..00af3889da 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -247,20 +247,16 @@ impl PlannerMode { }) .unwrap_or_else(|panic| { USING_CATCH_UNWIND.set(false); - Err( - apollo_federation::error::FederationError::SingleFederationError( - apollo_federation::error::SingleFederationError::Internal { - message: format!( - "query planner panicked: {}", - panic - .downcast_ref::() - .map(|s| s.as_str()) - .or_else(|| panic.downcast_ref::<&str>().copied()) - .unwrap_or_default() - ), - }, + Err(apollo_federation::error::FederationError::internal( + format!( + "query planner panicked: {}", + panic + .downcast_ref::() + .map(|s| s.as_str()) + .or_else(|| panic.downcast_ref::<&str>().copied()) + .unwrap_or_default() ), - ) + )) }); let js_result = js From e967b7597cea97d15937aaff6d5af2a35808245e Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Fri, 17 May 2024 21:01:20 +0200 Subject: [PATCH 06/10] Clarify README and docs.rs: apollo-federation crate is not for end users (#5196) --- apollo-federation/README.md | 19 ++++++++----------- apollo-federation/src/lib.rs | 15 +++++++++++++++ .../src/query_graph/build_query_graph.rs | 8 ++++---- apollo-federation/src/query_plan/operation.rs | 2 +- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/apollo-federation/README.md b/apollo-federation/README.md index 1dce0f1b1c..93781b480b 100644 --- a/apollo-federation/README.md +++ b/apollo-federation/README.md @@ -6,14 +6,19 @@ [![Join our Discord server](https://img.shields.io/discord/1022972389463687228.svg?color=7389D8&labelColor=6A7EC2&logo=discord&logoColor=ffffff&style=flat-square)](https://discord.gg/graphos) Apollo Federation ------------------------------ +----------------- Apollo Federation is an architecture for declaratively composing APIs into a unified graph. Each team can own their slice of the graph independently, empowering them to deliver autonomously and incrementally. Federation 2 is an evolution of the original Apollo Federation with an improved shared ownership model, enhanced type merging, and cleaner syntax for a smoother developer experience. It’s backwards compatible, requiring no major changes to your subgraphs. Checkout the [Federation 2 docs](https://www.apollographql.com/docs/federation) and [demo repo](https://github.com/apollographql/supergraph-demo-fed2) to take it for a spin and [let us know what you think](https://community.apollographql.com/t/announcing-apollo-federation-2/1821)! -## Versioning +## Usage + +This crate is internal to [Apollo Router](https://www.apollographql.com/docs/router/) +and not intended to be used directly. + +## Crate versioning The `apollo-federation` crate does **not** adhere to [Semantic Versioning](https://semver.org/). Any version may have breaking API changes, as this API is expected to only be used by `apollo-router`. @@ -23,17 +28,9 @@ This version number is **not** that of the Apollo Federation specification being See [Router documentation](https://www.apollographql.com/docs/router/federation-version-support/) for which Federation versions are supported by which Router versions. -## Usage - -TODO - -### CLI tool - -`cargo fed --help` - ## Contributing -TODO +See [contributing to the `apollo-router` repository](https://github.com/apollographql/router/blob/dev/CONTRIBUTING.md) ## Security diff --git a/apollo-federation/src/lib.rs b/apollo-federation/src/lib.rs index d6a87b11ea..9d2d621ab5 100644 --- a/apollo-federation/src/lib.rs +++ b/apollo-federation/src/lib.rs @@ -1,3 +1,18 @@ +//! ## Usage +//! +//! This crate is internal to [Apollo Router](https://www.apollographql.com/docs/router/) +//! and not intended to be used directly. +//! +//! ## Crate versioning +//! +//! The `apollo-federation` crate does **not** adhere to [Semantic Versioning](https://semver.org/). +//! Any version may have breaking API changes, as this API is expected to only be used by `apollo-router`. +//! Instead, the version number matches exactly that of the `apollo-router` crate version using it. +//! +//! This version number is **not** that of the Apollo Federation specification being implemented. +//! See [Router documentation](https://www.apollographql.com/docs/router/federation-version-support/) +//! for which Federation versions are supported by which Router versions. + #![allow(dead_code)] // TODO: This is fine while we're iterating, but should be removed later. mod api_schema; diff --git a/apollo-federation/src/query_graph/build_query_graph.rs b/apollo-federation/src/query_graph/build_query_graph.rs index bfd39e2c62..400aab95cb 100644 --- a/apollo-federation/src/query_graph/build_query_graph.rs +++ b/apollo-federation/src/query_graph/build_query_graph.rs @@ -1339,10 +1339,10 @@ impl FederatedQueryGraphBuilder { // like this way in the JS codebase, so we'll mimic the behavior for now. // // TODO: This is an optimization to avoid unnecessary inter-conversion between - // the apollo-rs operation representation and the federation-next one. This wasn't a + // the apollo-rs operation representation and the apollo-federation one. This wasn't a // problem in the JS codebase, as it would use its own operation representation from // the start. Eventually when operation processing code is ready and we make the switch - // to using the federation-next representation everywhere, we can probably simplify + // to using the apollo-federation representation everywhere, we can probably simplify // this. let new_conditions = if all_conditions.len() == 1 { all_conditions @@ -1410,10 +1410,10 @@ impl FederatedQueryGraphBuilder { // merge the selection sets before into one. // // TODO: This is an optimization to avoid unnecessary inter-conversion between - // the apollo-rs operation representation and the federation-next one. This wasn't a + // the apollo-rs operation representation and the apollo-federation one. This wasn't a // problem in the JS codebase, as it would use its own operation representation from // the start. Eventually when operation processing code is ready and we make the switch - // to using the federation-next representation everywhere, we can probably simplify + // to using the apollo-federation representation everywhere, we can probably simplify // this. let new_conditions = if all_conditions.len() == 1 { all_conditions diff --git a/apollo-federation/src/query_plan/operation.rs b/apollo-federation/src/query_plan/operation.rs index 1630545b57..026fa07bfc 100644 --- a/apollo-federation/src/query_plan/operation.rs +++ b/apollo-federation/src/query_plan/operation.rs @@ -139,7 +139,7 @@ fn same_directives(left: &executable::DirectiveList, right: &executable::Directi /// An analogue of the apollo-compiler type `Operation` with these changes: /// - Stores the schema that the operation is queried against. -/// - Swaps `operation_type` with `root_kind` (using the analogous federation-next type). +/// - Swaps `operation_type` with `root_kind` (using the analogous apollo-federation type). /// - Encloses collection types in `Arc`s to facilitate cheaper cloning. /// - Stores the fragments used by this operation (the executable document the operation was taken /// from may contain other fragments that are not used by this operation). From fdcbde28465139a91d0900cae20c7bf52d09c809 Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Fri, 17 May 2024 13:18:45 -0700 Subject: [PATCH 07/10] Add missing `From`/`TryFrom`s for schema positions (#5197) --- apollo-federation/src/query_graph/mod.rs | 26 + apollo-federation/src/schema/position.rs | 707 +++++++++++++++++------ 2 files changed, 542 insertions(+), 191 deletions(-) diff --git a/apollo-federation/src/query_graph/mod.rs b/apollo-federation/src/query_graph/mod.rs index 9f872e7644..01329b497e 100644 --- a/apollo-federation/src/query_graph/mod.rs +++ b/apollo-federation/src/query_graph/mod.rs @@ -104,6 +104,32 @@ impl Display for QueryGraphNodeType { } } +impl TryFrom for CompositeTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: QueryGraphNodeType) -> Result { + match value { + QueryGraphNodeType::SchemaType(ty) => ty.try_into(), + QueryGraphNodeType::FederatedRootType(_) => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not a composite type"# + ))), + } + } +} + +impl TryFrom for ObjectTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: QueryGraphNodeType) -> Result { + match value { + QueryGraphNodeType::SchemaType(ty) => ty.try_into(), + QueryGraphNodeType::FederatedRootType(_) => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an object type"# + ))), + } + } +} + #[derive(Debug, PartialEq, Clone)] pub(crate) struct QueryGraphEdge { /// Indicates what kind of edge this is and what the edge does/represents. For instance, if the diff --git a/apollo-federation/src/schema/position.rs b/apollo-federation/src/schema/position.rs index e61d227614..d5bb951112 100644 --- a/apollo-federation/src/schema/position.rs +++ b/apollo-federation/src/schema/position.rs @@ -32,7 +32,6 @@ use crate::error::SingleFederationError; use crate::link::database::links_metadata; use crate::link::federation_spec_definition::FEDERATION_INTERFACEOBJECT_DIRECTIVE_NAME_IN_SPEC; use crate::link::spec_definition::SpecDefinition; -use crate::query_graph::QueryGraphNodeType; use crate::schema::referencer::DirectiveReferencers; use crate::schema::referencer::EnumTypeReferencers; use crate::schema::referencer::InputObjectTypeReferencers; @@ -82,28 +81,20 @@ impl TypeDefinitionPosition { &self, schema: &'schema Schema, ) -> Result<&'schema ExtendedType, FederationError> { - let type_name = self.type_name(); - let type_ = schema + let ty = schema .types - .get(type_name) - .ok_or_else(|| SingleFederationError::Internal { - message: format!("Schema has no type \"{}\"", self), - })?; - let type_matches = match type_ { - ExtendedType::Scalar(_) => matches!(self, TypeDefinitionPosition::Scalar(_)), - ExtendedType::Object(_) => matches!(self, TypeDefinitionPosition::Object(_)), - ExtendedType::Interface(_) => matches!(self, TypeDefinitionPosition::Interface(_)), - ExtendedType::Union(_) => matches!(self, TypeDefinitionPosition::Union(_)), - ExtendedType::Enum(_) => matches!(self, TypeDefinitionPosition::Enum(_)), - ExtendedType::InputObject(_) => matches!(self, TypeDefinitionPosition::InputObject(_)), - }; - if type_matches { - Ok(type_) - } else { - Err(SingleFederationError::Internal { - message: format!("Schema type \"{}\" is the wrong kind", self), - } - .into()) + .get(self.type_name()) + .ok_or_else(|| FederationError::internal(format!(r#"Schema has no type "{self}""#)))?; + match (ty, self) { + (ExtendedType::Scalar(_), TypeDefinitionPosition::Scalar(_)) + | (ExtendedType::Object(_), TypeDefinitionPosition::Object(_)) + | (ExtendedType::Interface(_), TypeDefinitionPosition::Interface(_)) + | (ExtendedType::Union(_), TypeDefinitionPosition::Union(_)) + | (ExtendedType::Enum(_), TypeDefinitionPosition::Enum(_)) + | (ExtendedType::InputObject(_), TypeDefinitionPosition::InputObject(_)) => Ok(ty), + _ => Err(FederationError::internal(format!( + r#"Schema type "{self}" is the wrong kind"# + ))), } } @@ -115,6 +106,124 @@ impl TypeDefinitionPosition { } } +impl TryFrom for ScalarTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: TypeDefinitionPosition) -> Result { + match value { + TypeDefinitionPosition::Scalar(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not a scalar type"# + ))), + } + } +} + +impl TryFrom for ObjectTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: TypeDefinitionPosition) -> Result { + match value { + TypeDefinitionPosition::Object(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an object type"# + ))), + } + } +} + +impl TryFrom for InterfaceTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: TypeDefinitionPosition) -> Result { + match value { + TypeDefinitionPosition::Interface(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an interface type"# + ))), + } + } +} + +impl TryFrom for UnionTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: TypeDefinitionPosition) -> Result { + match value { + TypeDefinitionPosition::Union(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not a union type"# + ))), + } + } +} + +impl TryFrom for EnumTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: TypeDefinitionPosition) -> Result { + match value { + TypeDefinitionPosition::Enum(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an enum type"# + ))), + } + } +} + +impl TryFrom for InputObjectTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: TypeDefinitionPosition) -> Result { + match value { + TypeDefinitionPosition::InputObject(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an input object type"# + ))), + } + } +} + +impl From for TypeDefinitionPosition { + fn from(value: OutputTypeDefinitionPosition) -> Self { + match value { + OutputTypeDefinitionPosition::Scalar(value) => value.into(), + OutputTypeDefinitionPosition::Object(value) => value.into(), + OutputTypeDefinitionPosition::Interface(value) => value.into(), + OutputTypeDefinitionPosition::Union(value) => value.into(), + OutputTypeDefinitionPosition::Enum(value) => value.into(), + } + } +} + +impl From for TypeDefinitionPosition { + fn from(value: CompositeTypeDefinitionPosition) -> Self { + match value { + CompositeTypeDefinitionPosition::Object(value) => value.into(), + CompositeTypeDefinitionPosition::Interface(value) => value.into(), + CompositeTypeDefinitionPosition::Union(value) => value.into(), + } + } +} + +impl From for TypeDefinitionPosition { + fn from(value: AbstractTypeDefinitionPosition) -> Self { + match value { + AbstractTypeDefinitionPosition::Interface(value) => value.into(), + AbstractTypeDefinitionPosition::Union(value) => value.into(), + } + } +} + +impl From for TypeDefinitionPosition { + fn from(value: ObjectOrInterfaceTypeDefinitionPosition) -> Self { + match value { + ObjectOrInterfaceTypeDefinitionPosition::Object(value) => value.into(), + ObjectOrInterfaceTypeDefinitionPosition::Interface(value) => value.into(), + } + } +} + #[derive(Clone, PartialEq, Eq, Hash, derive_more::From, derive_more::Display)] pub(crate) enum OutputTypeDefinitionPosition { Scalar(ScalarTypeDefinitionPosition), @@ -151,23 +260,19 @@ impl OutputTypeDefinitionPosition { &self, schema: &'schema Schema, ) -> Result<&'schema ExtendedType, FederationError> { - let ty = - schema - .types - .get(self.type_name()) - .ok_or_else(|| SingleFederationError::Internal { - message: format!("Schema has no type \"{}\"", self), - })?; + let ty = schema + .types + .get(self.type_name()) + .ok_or_else(|| FederationError::internal(format!(r#"Schema has no type "{self}""#)))?; match (ty, self) { - (ExtendedType::Object(_), OutputTypeDefinitionPosition::Object(_)) + (ExtendedType::Scalar(_), OutputTypeDefinitionPosition::Scalar(_)) + | (ExtendedType::Object(_), OutputTypeDefinitionPosition::Object(_)) | (ExtendedType::Interface(_), OutputTypeDefinitionPosition::Interface(_)) | (ExtendedType::Union(_), OutputTypeDefinitionPosition::Union(_)) - | (ExtendedType::Scalar(_), OutputTypeDefinitionPosition::Scalar(_)) | (ExtendedType::Enum(_), OutputTypeDefinitionPosition::Enum(_)) => Ok(ty), - _ => Err(SingleFederationError::Internal { - message: format!("Schema type \"{}\" is the wrong kind", self), - } - .into()), + _ => Err(FederationError::internal(format!( + r#"Schema type "{self}" is the wrong kind"# + ))), } } @@ -179,26 +284,94 @@ impl OutputTypeDefinitionPosition { } } +impl TryFrom for ScalarTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: OutputTypeDefinitionPosition) -> Result { + match value { + OutputTypeDefinitionPosition::Scalar(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not a scalar type"# + ))), + } + } +} + +impl TryFrom for ObjectTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: OutputTypeDefinitionPosition) -> Result { + match value { + OutputTypeDefinitionPosition::Object(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an object type"# + ))), + } + } +} + +impl TryFrom for InterfaceTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: OutputTypeDefinitionPosition) -> Result { + match value { + OutputTypeDefinitionPosition::Interface(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an interface type"# + ))), + } + } +} + +impl TryFrom for UnionTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: OutputTypeDefinitionPosition) -> Result { + match value { + OutputTypeDefinitionPosition::Union(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not a union type"# + ))), + } + } +} + +impl TryFrom for EnumTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: OutputTypeDefinitionPosition) -> Result { + match value { + OutputTypeDefinitionPosition::Enum(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an enum type"# + ))), + } + } +} + impl TryFrom for OutputTypeDefinitionPosition { type Error = FederationError; fn try_from(value: TypeDefinitionPosition) -> Result { match value { - TypeDefinitionPosition::Scalar(value) => { - Ok(OutputTypeDefinitionPosition::Scalar(value)) - } - TypeDefinitionPosition::Object(value) => { - Ok(OutputTypeDefinitionPosition::Object(value)) - } - TypeDefinitionPosition::Interface(value) => { - Ok(OutputTypeDefinitionPosition::Interface(value)) - } - TypeDefinitionPosition::Union(value) => Ok(OutputTypeDefinitionPosition::Union(value)), - TypeDefinitionPosition::Enum(value) => Ok(OutputTypeDefinitionPosition::Enum(value)), - _ => Err(SingleFederationError::Internal { - message: format!("Type \"{}\" was unexpectedly not an output type", value,), - } - .into()), + TypeDefinitionPosition::Scalar(value) => Ok(value.into()), + TypeDefinitionPosition::Object(value) => Ok(value.into()), + TypeDefinitionPosition::Interface(value) => Ok(value.into()), + TypeDefinitionPosition::Enum(value) => Ok(value.into()), + TypeDefinitionPosition::Union(value) => Ok(value.into()), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an output type"# + ))), + } + } +} + +impl From for OutputTypeDefinitionPosition { + fn from(value: CompositeTypeDefinitionPosition) -> Self { + match value { + CompositeTypeDefinitionPosition::Object(value) => value.into(), + CompositeTypeDefinitionPosition::Interface(value) => value.into(), + CompositeTypeDefinitionPosition::Union(value) => value.into(), } } } @@ -206,12 +379,17 @@ impl TryFrom for OutputTypeDefinitionPosition { impl From for OutputTypeDefinitionPosition { fn from(value: AbstractTypeDefinitionPosition) -> Self { match value { - AbstractTypeDefinitionPosition::Interface(value) => { - OutputTypeDefinitionPosition::Interface(value) - } - AbstractTypeDefinitionPosition::Union(value) => { - OutputTypeDefinitionPosition::Union(value) - } + AbstractTypeDefinitionPosition::Interface(value) => value.into(), + AbstractTypeDefinitionPosition::Union(value) => value.into(), + } + } +} + +impl From for OutputTypeDefinitionPosition { + fn from(value: ObjectOrInterfaceTypeDefinitionPosition) -> Self { + match value { + ObjectOrInterfaceTypeDefinitionPosition::Object(value) => value.into(), + ObjectOrInterfaceTypeDefinitionPosition::Interface(value) => value.into(), } } } @@ -270,14 +448,11 @@ impl CompositeTypeDefinitionPosition { if *field.field_name() == field_name { Ok(field.into()) } else { - Err(SingleFederationError::Internal { - message: format!( - "Union types don't have field \"{}\", only \"{}\"", - field_name, - field.field_name(), - ), - } - .into()) + Err(FederationError::internal(format!( + r#"Union types don't have field "{}", only "{}""#, + field_name, + field.field_name(), + ))) } } } @@ -301,28 +476,17 @@ impl CompositeTypeDefinitionPosition { &self, schema: &'schema Schema, ) -> Result<&'schema ExtendedType, FederationError> { - let type_name = self.type_name(); - let type_ = schema + let ty = schema .types - .get(type_name) - .ok_or_else(|| SingleFederationError::Internal { - message: format!("Schema has no type \"{}\"", self), - })?; - let type_matches = match type_ { - ExtendedType::Object(_) => matches!(self, CompositeTypeDefinitionPosition::Object(_)), - ExtendedType::Interface(_) => { - matches!(self, CompositeTypeDefinitionPosition::Interface(_)) - } - ExtendedType::Union(_) => matches!(self, CompositeTypeDefinitionPosition::Union(_)), - _ => false, - }; - if type_matches { - Ok(type_) - } else { - Err(SingleFederationError::Internal { - message: format!("Schema type \"{}\" is the wrong kind", self), - } - .into()) + .get(self.type_name()) + .ok_or_else(|| FederationError::internal(format!(r#"Schema has no type "{self}""#)))?; + match (ty, self) { + (ExtendedType::Object(_), CompositeTypeDefinitionPosition::Object(_)) + | (ExtendedType::Interface(_), CompositeTypeDefinitionPosition::Interface(_)) + | (ExtendedType::Union(_), CompositeTypeDefinitionPosition::Union(_)) => Ok(ty), + _ => Err(FederationError::internal(format!( + r#"Schema type "{self}" is the wrong kind"# + ))), } } @@ -343,115 +507,89 @@ impl CompositeTypeDefinitionPosition { } } -impl TryFrom for CompositeTypeDefinitionPosition { +impl TryFrom for ObjectTypeDefinitionPosition { type Error = FederationError; - fn try_from(value: TypeDefinitionPosition) -> Result { + fn try_from(value: CompositeTypeDefinitionPosition) -> Result { match value { - TypeDefinitionPosition::Object(value) => { - Ok(CompositeTypeDefinitionPosition::Object(value)) - } - TypeDefinitionPosition::Interface(value) => { - Ok(CompositeTypeDefinitionPosition::Interface(value)) - } - TypeDefinitionPosition::Union(value) => { - Ok(CompositeTypeDefinitionPosition::Union(value)) - } - _ => Err(SingleFederationError::Internal { - message: format!(r#"Type "{value}" was unexpectedly not a composite type"#), - } - .into()), + CompositeTypeDefinitionPosition::Object(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an object type"# + ))), } } } -impl TryFrom for CompositeTypeDefinitionPosition { +impl TryFrom for InterfaceTypeDefinitionPosition { type Error = FederationError; - fn try_from(value: OutputTypeDefinitionPosition) -> Result { + fn try_from(value: CompositeTypeDefinitionPosition) -> Result { match value { - OutputTypeDefinitionPosition::Object(value) => { - Ok(CompositeTypeDefinitionPosition::Object(value)) - } - OutputTypeDefinitionPosition::Interface(value) => { - Ok(CompositeTypeDefinitionPosition::Interface(value)) - } - OutputTypeDefinitionPosition::Union(value) => { - Ok(CompositeTypeDefinitionPosition::Union(value)) - } + CompositeTypeDefinitionPosition::Interface(value) => Ok(value), _ => Err(FederationError::internal(format!( - "Type `{value}` was unexpectedly not a composite type" + r#"Type "{value}" was unexpectedly not an interface type"# ))), } } } -impl TryFrom for ObjectTypeDefinitionPosition { +impl TryFrom for UnionTypeDefinitionPosition { type Error = FederationError; - fn try_from(value: OutputTypeDefinitionPosition) -> Result { + fn try_from(value: CompositeTypeDefinitionPosition) -> Result { match value { - OutputTypeDefinitionPosition::Object(value) => Ok(value), + CompositeTypeDefinitionPosition::Union(value) => Ok(value), _ => Err(FederationError::internal(format!( - "Type `{value}` was unexpectedly not an object type" + r#"Type "{value}" was unexpectedly not a union type"# ))), } } } -impl From for CompositeTypeDefinitionPosition { - fn from(value: AbstractTypeDefinitionPosition) -> Self { - match value { - AbstractTypeDefinitionPosition::Interface(value) => value.into(), - AbstractTypeDefinitionPosition::Union(value) => value.into(), - } - } -} +impl TryFrom for CompositeTypeDefinitionPosition { + type Error = FederationError; -impl From for CompositeTypeDefinitionPosition { - fn from(value: ObjectOrInterfaceTypeDefinitionPosition) -> Self { + fn try_from(value: TypeDefinitionPosition) -> Result { match value { - ObjectOrInterfaceTypeDefinitionPosition::Object(value) => value.into(), - ObjectOrInterfaceTypeDefinitionPosition::Interface(value) => value.into(), + TypeDefinitionPosition::Object(value) => Ok(value.into()), + TypeDefinitionPosition::Interface(value) => Ok(value.into()), + TypeDefinitionPosition::Union(value) => Ok(value.into()), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not a composite type"# + ))), } } } -impl TryFrom for CompositeTypeDefinitionPosition { +impl TryFrom for CompositeTypeDefinitionPosition { type Error = FederationError; - fn try_from(value: QueryGraphNodeType) -> Result { + fn try_from(value: OutputTypeDefinitionPosition) -> Result { match value { - QueryGraphNodeType::SchemaType(ty) => ty.try_into(), - QueryGraphNodeType::FederatedRootType(_) => Err(FederationError::internal(format!( - "Type `{value}` was unexpectedly not a composite type" + OutputTypeDefinitionPosition::Object(value) => Ok(value.into()), + OutputTypeDefinitionPosition::Interface(value) => Ok(value.into()), + OutputTypeDefinitionPosition::Union(value) => Ok(value.into()), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not a composite type"# ))), } } } -impl TryFrom for ObjectTypeDefinitionPosition { - type Error = FederationError; - - fn try_from(value: QueryGraphNodeType) -> Result { +impl From for CompositeTypeDefinitionPosition { + fn from(value: AbstractTypeDefinitionPosition) -> Self { match value { - QueryGraphNodeType::SchemaType(ty) => ty.try_into(), - QueryGraphNodeType::FederatedRootType(_) => Err(FederationError::internal(format!( - "Type `{value}` was unexpectedly not a composite type" - ))), + AbstractTypeDefinitionPosition::Interface(value) => value.into(), + AbstractTypeDefinitionPosition::Union(value) => value.into(), } } } -impl TryFrom for ObjectTypeDefinitionPosition { - type Error = FederationError; - - fn try_from(value: CompositeTypeDefinitionPosition) -> Result { +impl From for CompositeTypeDefinitionPosition { + fn from(value: ObjectOrInterfaceTypeDefinitionPosition) -> Self { match value { - CompositeTypeDefinitionPosition::Object(value) => Ok(value), - _ => Err(FederationError::internal(format!( - "Type `{value}` was unexpectedly not an object type" - ))), + ObjectOrInterfaceTypeDefinitionPosition::Object(value) => value.into(), + ObjectOrInterfaceTypeDefinitionPosition::Interface(value) => value.into(), } } } @@ -478,6 +616,88 @@ impl AbstractTypeDefinitionPosition { AbstractTypeDefinitionPosition::Union(type_) => &type_.type_name, } } + + pub(crate) fn field( + &self, + field_name: Name, + ) -> Result { + match self { + AbstractTypeDefinitionPosition::Interface(type_) => Ok(type_.field(field_name).into()), + AbstractTypeDefinitionPosition::Union(type_) => { + let field = type_.introspection_typename_field(); + if *field.field_name() == field_name { + Ok(field.into()) + } else { + Err(FederationError::internal(format!( + r#"Union types don't have field "{}", only "{}""#, + field_name, + field.field_name(), + ))) + } + } + } + } + + pub(crate) fn introspection_typename_field(&self) -> FieldDefinitionPosition { + match self { + AbstractTypeDefinitionPosition::Interface(type_) => { + type_.introspection_typename_field().into() + } + AbstractTypeDefinitionPosition::Union(type_) => { + type_.introspection_typename_field().into() + } + } + } + + pub(crate) fn get<'schema>( + &self, + schema: &'schema Schema, + ) -> Result<&'schema ExtendedType, FederationError> { + let ty = schema + .types + .get(self.type_name()) + .ok_or_else(|| FederationError::internal(format!(r#"Schema has no type "{self}""#)))?; + match (ty, self) { + (ExtendedType::Interface(_), AbstractTypeDefinitionPosition::Interface(_)) + | (ExtendedType::Union(_), AbstractTypeDefinitionPosition::Union(_)) => Ok(ty), + _ => Err(FederationError::internal(format!( + r#"Schema type "{self}" is the wrong kind"# + ))), + } + } + + pub(crate) fn try_get<'schema>( + &self, + schema: &'schema Schema, + ) -> Option<&'schema ExtendedType> { + self.get(schema).ok() + } +} + +impl TryFrom for InterfaceTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: AbstractTypeDefinitionPosition) -> Result { + match value { + AbstractTypeDefinitionPosition::Interface(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an interface type"# + ))), + } + } +} + +impl TryFrom for UnionTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: AbstractTypeDefinitionPosition) -> Result { + match value { + AbstractTypeDefinitionPosition::Union(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not a union type"# + ))), + } + } } impl TryFrom for AbstractTypeDefinitionPosition { @@ -485,15 +705,10 @@ impl TryFrom for AbstractTypeDefinitionPosition { fn try_from(value: TypeDefinitionPosition) -> Result { match value { - TypeDefinitionPosition::Interface(value) => { - Ok(AbstractTypeDefinitionPosition::Interface(value)) - } - TypeDefinitionPosition::Union(value) => { - Ok(AbstractTypeDefinitionPosition::Union(value)) - } + TypeDefinitionPosition::Interface(value) => Ok(value.into()), + TypeDefinitionPosition::Union(value) => Ok(value.into()), _ => Err(FederationError::internal(format!( - "Type \"{}\" was unexpectedly not an interface/union type", - value, + r#"Type "{value}" was unexpectedly not an abstract type"# ))), } } @@ -504,15 +719,37 @@ impl TryFrom for AbstractTypeDefinitionPosition { fn try_from(value: OutputTypeDefinitionPosition) -> Result { match value { - OutputTypeDefinitionPosition::Interface(value) => { - Ok(AbstractTypeDefinitionPosition::Interface(value)) - } - OutputTypeDefinitionPosition::Union(value) => { - Ok(AbstractTypeDefinitionPosition::Union(value)) - } + OutputTypeDefinitionPosition::Interface(value) => Ok(value.into()), + OutputTypeDefinitionPosition::Union(value) => Ok(value.into()), _ => Err(FederationError::internal(format!( - "Type \"{}\" was unexpectedly not an interface/union type", - value, + r#"Type "{value}" was unexpectedly not an abstract type"# + ))), + } + } +} + +impl TryFrom for AbstractTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: CompositeTypeDefinitionPosition) -> Result { + match value { + CompositeTypeDefinitionPosition::Interface(value) => Ok(value.into()), + CompositeTypeDefinitionPosition::Union(value) => Ok(value.into()), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an abstract type"# + ))), + } + } +} + +impl TryFrom for AbstractTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: ObjectOrInterfaceTypeDefinitionPosition) -> Result { + match value { + ObjectOrInterfaceTypeDefinitionPosition::Interface(value) => Ok(value.into()), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an abstract type"# ))), } } @@ -551,6 +788,69 @@ impl ObjectOrInterfaceTypeDefinitionPosition { } } } + + pub(crate) fn introspection_typename_field(&self) -> FieldDefinitionPosition { + match self { + ObjectOrInterfaceTypeDefinitionPosition::Object(type_) => { + type_.introspection_typename_field().into() + } + ObjectOrInterfaceTypeDefinitionPosition::Interface(type_) => { + type_.introspection_typename_field().into() + } + } + } + + pub(crate) fn get<'schema>( + &self, + schema: &'schema Schema, + ) -> Result<&'schema ExtendedType, FederationError> { + let ty = schema + .types + .get(self.type_name()) + .ok_or_else(|| FederationError::internal(format!(r#"Schema has no type "{self}""#)))?; + match (ty, self) { + (ExtendedType::Object(_), ObjectOrInterfaceTypeDefinitionPosition::Object(_)) + | (ExtendedType::Interface(_), ObjectOrInterfaceTypeDefinitionPosition::Interface(_)) => { + Ok(ty) + } + _ => Err(FederationError::internal(format!( + r#"Schema type "{self}" is the wrong kind"# + ))), + } + } + + pub(crate) fn try_get<'schema>( + &self, + schema: &'schema Schema, + ) -> Option<&'schema ExtendedType> { + self.get(schema).ok() + } +} + +impl TryFrom for ObjectTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: ObjectOrInterfaceTypeDefinitionPosition) -> Result { + match value { + ObjectOrInterfaceTypeDefinitionPosition::Object(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an object type"# + ))), + } + } +} + +impl TryFrom for InterfaceTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: ObjectOrInterfaceTypeDefinitionPosition) -> Result { + match value { + ObjectOrInterfaceTypeDefinitionPosition::Interface(value) => Ok(value), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an interface type"# + ))), + } + } } impl TryFrom for ObjectOrInterfaceTypeDefinitionPosition { @@ -558,19 +858,11 @@ impl TryFrom for ObjectOrInterfaceTypeDefinitionPosition fn try_from(value: TypeDefinitionPosition) -> Result { match value { - TypeDefinitionPosition::Object(value) => { - Ok(ObjectOrInterfaceTypeDefinitionPosition::Object(value)) - } - TypeDefinitionPosition::Interface(value) => { - Ok(ObjectOrInterfaceTypeDefinitionPosition::Interface(value)) - } - _ => Err(SingleFederationError::Internal { - message: format!( - "Type \"{}\" was unexpectedly not an object/interface type", - value, - ), - } - .into()), + TypeDefinitionPosition::Object(value) => Ok(value.into()), + TypeDefinitionPosition::Interface(value) => Ok(value.into()), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an object/interface type"# + ))), } } } @@ -580,19 +872,38 @@ impl TryFrom for ObjectOrInterfaceTypeDefinitionPo fn try_from(value: OutputTypeDefinitionPosition) -> Result { match value { - OutputTypeDefinitionPosition::Object(value) => { - Ok(ObjectOrInterfaceTypeDefinitionPosition::Object(value)) - } - OutputTypeDefinitionPosition::Interface(value) => { - Ok(ObjectOrInterfaceTypeDefinitionPosition::Interface(value)) - } - _ => Err(SingleFederationError::Internal { - message: format!( - "Output type \"{}\" was unexpectedly not an object/interface type", - value, - ), - } - .into()), + OutputTypeDefinitionPosition::Object(value) => Ok(value.into()), + OutputTypeDefinitionPosition::Interface(value) => Ok(value.into()), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an object/interface type"# + ))), + } + } +} + +impl TryFrom for ObjectOrInterfaceTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: CompositeTypeDefinitionPosition) -> Result { + match value { + CompositeTypeDefinitionPosition::Object(value) => Ok(value.into()), + CompositeTypeDefinitionPosition::Interface(value) => Ok(value.into()), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an object/interface type"# + ))), + } + } +} + +impl TryFrom for ObjectOrInterfaceTypeDefinitionPosition { + type Error = FederationError; + + fn try_from(value: AbstractTypeDefinitionPosition) -> Result { + match value { + AbstractTypeDefinitionPosition::Interface(value) => Ok(value.into()), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an object/interface type"# + ))), } } } @@ -746,6 +1057,20 @@ impl ObjectOrInterfaceFieldDefinitionPosition { } } +impl TryFrom for ObjectOrInterfaceFieldDefinitionPosition { + type Error = FederationError; + + fn try_from(value: FieldDefinitionPosition) -> Result { + match value { + FieldDefinitionPosition::Object(value) => Ok(value.into()), + FieldDefinitionPosition::Interface(value) => Ok(value.into()), + _ => Err(FederationError::internal(format!( + r#"Type "{value}" was unexpectedly not an object/interface field"# + ))), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) struct SchemaDefinitionPosition; From 2507a4a18928b8f2da39b21e889637ada2de4b32 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Mon, 20 May 2024 18:27:37 +0300 Subject: [PATCH 08/10] merge back from main to bring docs changes back onto dev. (#5203) Co-authored-by: Coenen Benjamin --- docs/source/federation-version-support.mdx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/source/federation-version-support.mdx b/docs/source/federation-version-support.mdx index 44a7c65a72..7205400590 100644 --- a/docs/source/federation-version-support.mdx +++ b/docs/source/federation-version-support.mdx @@ -43,7 +43,15 @@ The table below shows which version of federation each router release is compile - v1.45.0 + v1.45.1 + + + 2.7.2 + + + + + ⚠️ v1.45.0 2.7.2 From 18a5b6ad6479be2445f598d7fb838788449e67cf Mon Sep 17 00:00:00 2001 From: Gary Pennington Date: Tue, 21 May 2024 08:49:43 +0100 Subject: [PATCH 09/10] Remove unused in house router configuration document (#5176) --- docs/shared/router-common-config.mdx | 210 --------------------------- 1 file changed, 210 deletions(-) delete mode 100644 docs/shared/router-common-config.mdx diff --git a/docs/shared/router-common-config.mdx b/docs/shared/router-common-config.mdx deleted file mode 100644 index 6eb5cd12ba..0000000000 --- a/docs/shared/router-common-config.mdx +++ /dev/null @@ -1,210 +0,0 @@ -```yaml -# Common values. Overridden by in-house configurations. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -# Anything set here applies to the in-house-router chart, and -# all configuration that set here is pushed to all sub-charts -# because of the way Helm works: -# https://helm.sh/docs/chart_template_guide/subcharts_and_globals/ - -# Set fullnameOverride here for virtualservice to use -fullnameOverride: router - -router: - serviceAccount: - # The service account is created permissions.yaml - create: false - name: router - fullnameOverride: router - router: - args: [--hot-reload] - configuration: - limits: - http_max_request_bytes: 20000000 #20MB - traffic_shaping: - router: - timeout: 6min - all: - experimental_enable_http2: false # to disable for all subgraphs - timeout: 5min - supergraph: - introspection: true - # this intentionally disables `@defer` support in the router until - # until the transition from the gateway to the router is complete. - defer_support: false - listen: 0.0.0.0:80 - path: /graphql - # We recommend keeping subgraph errors on in your own environment, always. - include_subgraph_errors: - all: true - cors: - allow_credentials: true - expose_headers: - - logged_in - - identity - headers: - all: - request: - - propagate: - named: "x-api-key" - - propagate: - named: "apollo-sudo" - - propagate: - named: "x-request-id" - - propagate: - named: "x-ot-span-context" - - propagate: - named: "x-cloud-trace-context" - - propagate: - named: "grpc-trace-bin" - subgraphs: - linter: - request: - - propagate: - named: apollographql-client-name - - propagate: - named: apollographql-client-version - - propagate: - named: apollo-client-name - - propagate: - named: apollo-client-version - kotlin: - request: - - propagate: - named: apollographql-client-name - - propagate: - named: apollographql-client-version - - propagate: - named: apollo-client-name - - propagate: - named: apollo-client-version - rhai: - scripts: /dist/rhai - main: in-house.rhai - telemetry: - apollo: - batch_processor: - # default 5s - scheduled_delay: 3s - # default 1 - max_concurrent_exports: 100 - max_export_batch_size: 512 - # default 30s - max_export_timeout: 10s - max_queue_size: 2048 - metrics: - prometheus: - enabled: false - otlp: - temporality: delta - endpoint: "${env.NODE_IP}:4317" - # Enabling this both includes the `apollo-trace-id` value on the HTTP response, - # but also creates "Response Header" nodes in the trace that we send to Studio. - # This allows us to correlate traces from Studio to logs: - # https://www.apollographql.com/docs/router/configuration/tracing/#trace-id - tracing: - propagation: - zipkin: true - trace_context: true - experimental_response_trace_id: - enabled: true - coprocessor: - timeout: 3s - url: http://127.0.0.1:4001 - router: - request: - body: true - headers: true - method: true - path: true - response: - body: true - context: true - headers: true - status_code: true - subgraph: - all: - response: - headers: true - body: true - extraContainers: - - name: apilogger - image: "us-central1-docker.pkg.dev/platform-cross-environment/platform-docker/apilogger:{{ .Values.global.apilogger.release }}" - imagePullPolicy: IfNotPresent - ports: - - containerPort: 4001 - env: - - name: NODE_OPTIONS - value: "--max-old-space-size=768" - - name: NODE_ENV - value: production - - name: NODE_NAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: STACK - value: "{{ .Values.global.apilogger.stack }}" - resources: - limits: - cpu: "1.5" - memory: 1Gi - requests: - cpu: "1" - memory: 1Gi - extraLabels: - tags.datadoghq.com/service: "router" - tags.datadoghq.com/version: "{{ .Chart.AppVersion | quote }}" - extraVolumeMounts: - - name: rhai-volume - mountPath: /dist/rhai - readOnly: true - extraVolumes: - - name: rhai-volume - configMap: - # Provide the name of the ConfigMap containing the files - # to add to the container - name: rhai-config - probes: - # -- Configure readiness probe - readiness: - initialDelaySeconds: 1 - # -- Configure liveness probe - liveness: - initialDelaySeconds: 1 - autoscaling: - enabled: true - minReplicas: 2 - maxReplicas: 12 - targetCPUUtilizationPercentage: 75 - affinity: - podAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 50 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app.kubernetes.io/name - operator: In - values: - - router-domain-ingress - - router-internal-domain-ingress - topologyKey: "kubernetes.io/hostname" - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app.kubernetes.io/name - operator: NotIn - values: - - router - topologyKey: "kubernetes.io/hostname" - podDisruptionBudget: - minAvailable: 2 -``` From 8b22c403e2ae5b18e3a7a090bc76db8d4ac476bd Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Tue, 21 May 2024 04:24:37 -0700 Subject: [PATCH 10/10] Fixed a subtle bug in add_at_path method (#5200) Co-authored-by: Iryna Shestak --- apollo-federation/src/query_graph/graph_path.rs | 9 +++++++++ apollo-federation/src/query_plan/operation.rs | 16 +++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/apollo-federation/src/query_graph/graph_path.rs b/apollo-federation/src/query_graph/graph_path.rs index 8127b3dfc8..12d76af785 100644 --- a/apollo-federation/src/query_graph/graph_path.rs +++ b/apollo-federation/src/query_graph/graph_path.rs @@ -333,6 +333,15 @@ impl OpPathElement { } } + pub(crate) fn sub_selection_type_position( + &self, + ) -> Result, FederationError> { + match self { + OpPathElement::Field(field) => Ok(field.data().output_base_type()?.try_into().ok()), + OpPathElement::InlineFragment(inline) => Ok(Some(inline.data().casted_type())), + } + } + pub(crate) fn extract_operation_conditionals( &self, ) -> Result, FederationError> { diff --git a/apollo-federation/src/query_plan/operation.rs b/apollo-federation/src/query_plan/operation.rs index 026fa07bfc..dfcde55cab 100644 --- a/apollo-federation/src/query_plan/operation.rs +++ b/apollo-federation/src/query_plan/operation.rs @@ -1781,7 +1781,7 @@ mod normalized_inline_fragment_selection { } } - pub(super) fn casted_type(&self) -> CompositeTypeDefinitionPosition { + pub(crate) fn casted_type(&self) -> CompositeTypeDefinitionPosition { self.type_condition_position .clone() .unwrap_or_else(|| self.parent_type_position.clone()) @@ -2531,10 +2531,8 @@ impl SelectionSet { "Unable to rebase selection updates", )); }; - let sub_selection_parent_type: Option = match element { - OpPathElement::Field(ref field) => field.data().output_base_type()?.try_into().ok(), - OpPathElement::InlineFragment(ref inline) => Some(inline.data().casted_type()), - }; + let sub_selection_parent_type: Option = + element.sub_selection_type_position()?; let Some(ref sub_selection_parent_type) = sub_selection_parent_type else { // This is a leaf, so all updates should correspond ot the same field and we just use the first. @@ -2786,6 +2784,9 @@ impl SelectionSet { match path.split_first() { // If we have a sub-path, recurse. Some((ele, path @ &[_, ..])) => { + let Some(sub_selection_type) = ele.sub_selection_type_position()? else { + return Err(FederationError::internal("unexpected error: add_at_path encountered a field that is not of a composite type".to_string())); + }; let mut selection = Arc::make_mut(&mut self.selections) .entry(ele.key()) .or_insert(|| { @@ -2793,10 +2794,7 @@ impl SelectionSet { OpPathElement::clone(ele), // We immediately add a selection afterward to make this selection set // valid. - Some(SelectionSet::empty( - self.schema.clone(), - self.type_position.clone(), - )), + Some(SelectionSet::empty(self.schema.clone(), sub_selection_type)), ) })?; match &mut selection {