diff --git a/.changesets/helm_host_configuration.md b/.changesets/helm_host_configuration.md new file mode 100644 index 0000000000..e26bdc6dd1 --- /dev/null +++ b/.changesets/helm_host_configuration.md @@ -0,0 +1,5 @@ +### Allow configuring host via Helm template for virtual service ([PR #5545](https://github.com/apollographql/router/pull/5795)) + +When deploying via Helm, you can now configure hosts in `virtualservice.yaml` as a single host or a range of hosts. This is helpful when different hosts could be used within a cluster. + +By [@nicksephora](https://github.com/nicksephora) in https://github.com/apollographql/router/pull/5545 diff --git a/.circleci/config.yml b/.circleci/config.yml index 91bce18b05..126c85a25d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,6 @@ version: 2.1 -# Cache key bump: 1 +# Cache key bump: 2 # These "CircleCI Orbs" are reusable bits of configuration that can be shared # across projects. See https://circleci.com/orbs/ for more information. @@ -48,18 +48,14 @@ executors: # See https://circleci.com/docs/xcode-policy along with the support matrix # at https://circleci.com/docs/using-macos#supported-xcode-versions. # We use the major.minor notation to bring in compatible patches. - xcode: "14.2.0" + xcode: "15.4.0" resource_class: macos.m1.large.gen1 macos_test: &macos_test_executor macos: # See https://circleci.com/docs/xcode-policy along with the support matrix # at https://circleci.com/docs/using-macos#supported-xcode-versions. # We use the major.minor notation to bring in compatible patches. - # - # TODO: remove workaround added in https://github.com/apollographql/router/pull/5462 - # once we update to Xcode >= 15.1.0 - # See: https://github.com/apollographql/router/pull/5462 - xcode: "14.2.0" + xcode: "15.4.0" resource_class: macos.m1.large.gen1 windows_build: &windows_build_executor machine: @@ -76,16 +72,20 @@ executors: parameters: toolchain_version: type: string - default: '{{ checksum ".circleci/config.yml" }}-v2-{{ checksum "~/.arch" }}-{{ checksum "rust-toolchain.toml" }}-{{ checksum "~/.daily_version" }}' + default: '{{ checksum ".circleci/config.yml" }}-v3-{{ checksum "~/.arch" }}-{{ checksum "rust-toolchain.toml" }}-{{ checksum "~/.daily_version" }}' xtask_version: type: string - default: '{{ checksum ".circleci/config.yml" }}-{{ checksum "~/.arch" }}-{{ checksum "rust-toolchain.toml" }}-{{ checksum "~/.xtask_version" }}' + default: '{{ checksum ".circleci/config.yml" }}-v3-{{ checksum "~/.arch" }}-{{ checksum "rust-toolchain.toml" }}-{{ checksum "~/.xtask_version" }}' merge_version: type: string - default: '{{ checksum ".circleci/config.yml" }}-{{ checksum "~/.arch" }}-{{ checksum "rust-toolchain.toml" }}-{{ checksum "~/.xtask_version" }}-{{ checksum "~/.merge_version" }}' + default: '{{ checksum ".circleci/config.yml" }}-v3-{{ checksum "~/.arch" }}-{{ checksum "rust-toolchain.toml" }}-{{ checksum "~/.xtask_version" }}-{{ checksum "~/.merge_version" }}' protoc_version: type: string default: "21.8" + # note the cmake version is only used for manual installs, not for installs from a package manager like apt or homebrew + cmake_version: + type: string + default: "3.31.1" nightly: type: boolean default: false @@ -93,6 +93,9 @@ parameters: quick_nightly: type: boolean default: false + test_updated_cargo_deps: + type: boolean + default: false # These are common environment variables that we want to set on on all jobs. # While these could conceivably be set on the CircleCI project settings' @@ -246,9 +249,15 @@ commands: - run: name: Install CMake command: | - choco install cmake.install -y - echo 'export PATH="/c/Program Files/CMake/bin:$PATH"' >> "$BASH_ENV" - exit $LASTEXITCODE + mkdir -p "$HOME/.local" + if [[ ! -f "$HOME/.local/bin/cmake" ]]; then + curl -L https://github.com/Kitware/CMake/releases/download/v<< pipeline.parameters.cmake_version >>/cmake-<< pipeline.parameters.cmake_version >>-windows-x86_64.zip --output cmake.zip + # The zip file has a root directory, so we put it somewhere else first before placing the files in .local + unzip cmake.zip -d /tmp > /dev/null + cp /tmp/cmake-<< pipeline.parameters.cmake_version >>-windows-x86_64/* -R "$HOME/.local" + fi + + cmake --version - when: condition: or: @@ -503,11 +512,7 @@ commands: environment: # Use the settings from the "ci" profile in nextest configuration. NEXTEST_PROFILE: ci - # Temporary disable lib backtrace since it crashing on MacOS - # TODO: remove this workaround once we update to Xcode >= 15.1.0 - # See: https://github.com/apollographql/router/pull/5462 - RUST_LIB_BACKTRACE: 0 - command: xtask test --workspace --locked --features ci + command: xtask test --workspace --locked --features ci,hyper_header_limits - run: name: Delete large files from cache command: | @@ -599,6 +604,10 @@ jobs: parameters: platform: type: executor + default: amd_linux_test + from_test_updated_cargo_deps_workflow: + type: boolean + default: false executor: << parameters.platform >> steps: - checkout @@ -611,6 +620,67 @@ jobs: cargo fetch - xtask_test: variant: "updated" + + - when: + condition: + equal: [ true, << parameters.from_test_updated_cargo_deps_workflow >> ] + steps: + - slack/notify: + event: fail + custom: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":x: The `test_updated_cargo_deps` workflow has **failed** for `${CIRCLE_JOB}` on `${CIRCLE_PROJECT_REPONAME}`'s `${CIRCLE_BRANCH}`!" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "action_id": "success_tagged_deploy_view", + "text": { + "type": "plain_text", + "text": "View Job" + }, + "url": "${CIRCLE_BUILD_URL}" + } + ] + } + ] + } + - slack/notify: + event: pass + custom: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":white_check_mark: The `test_updated_cargo_deps` workflow has passed for `${CIRCLE_JOB}` on `${CIRCLE_PROJECT_REPONAME}`'s `${CIRCLE_BRANCH}`." + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "action_id": "success_tagged_deploy_view", + "text": { + "type": "plain_text", + "text": "View Job" + }, + "url": "${CIRCLE_BUILD_URL}" + } + ] + } + ] + } pre_verify_release: environment: <<: *common_job_environment @@ -655,10 +725,10 @@ jobs: - run: cargo xtask release prepare nightly - run: command: > - cargo xtask dist --target aarch64-apple-darwin + cargo xtask dist --target aarch64-apple-darwin --features hyper_header_limits - run: command: > - cargo xtask dist --target x86_64-apple-darwin + cargo xtask dist --target x86_64-apple-darwin --features hyper_header_limits - run: command: > mkdir -p artifacts @@ -718,7 +788,7 @@ jobs: - run: cargo xtask release prepare nightly - run: command: > - cargo xtask dist + cargo xtask dist --features hyper_header_limits - run: command: > mkdir -p artifacts @@ -750,20 +820,20 @@ jobs: command: | # Source of the new image will be ser to the repo URL. # This will have the effect of setting org.opencontainers.image.source and org.opencontainers.image.author to the originating pipeline - # Therefore the docker image will have the same permissions as the originating project. + # Therefore the docker image will have the same permissions as the originating project. # See: https://docs.github.com/en/packages/learn-github-packages/connecting-a-repository-to-a-package#connecting-a-repository-to-a-container-image-using-the-command-line - + BASE_VERSION=$(cargo metadata --format-version=1 --no-deps | jq --raw-output '.packages[0].version') ARTIFACT_URL="https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0/artifacts/router-v${BASE_VERSION}-x86_64-unknown-linux-gnu.tar.gz" VERSION="v$(echo "${BASE_VERSION}" | tr "+" "-")" ROUTER_TAG=ghcr.io/apollographql/nightly/router - + echo "REPO_URL: ${REPO_URL}" echo "BASE_VERSION: ${BASE_VERSION}" echo "ARTIFACT_URL: ${ARTIFACT_URL}" echo "VERSION: ${VERSION}" echo "ROUTER_TAG: ${ROUTER_TAG}" - + # Create a multi-arch builder which works properly under qemu docker run --rm --privileged multiarch/qemu-user-static --reset -p yes docker context create buildx-build @@ -948,12 +1018,19 @@ jobs: helm push ${CHART} oci://ghcr.io/apollographql/helm-charts workflows: + test_updated_cargo_deps: + when: << pipeline.parameters.test_updated_cargo_deps >> + jobs: + - test_updated: + platform: amd_linux_test + from_test_updated_cargo_deps_workflow: true ci_checks: when: not: or: - << pipeline.parameters.nightly >> - << pipeline.parameters.quick_nightly >> + - << pipeline.parameters.test_updated_cargo_deps >> jobs: - lint: matrix: @@ -968,15 +1045,6 @@ workflows: parameters: platform: [ amd_linux_build ] - - test_updated: - requires: - - lint - - check_helm - - check_compliance - matrix: - parameters: - platform: - [ amd_linux_test ] - test: # this should be changed back to true on dev after release fuzz: false @@ -1016,21 +1084,9 @@ workflows: matrix: parameters: platform: [ amd_linux_build ] - - - test_updated: - requires: - - lint - - check_helm - - check_compliance - matrix: - parameters: - platform: - [ amd_linux_test ] - test: requires: - lint - - check_helm - - check_compliance matrix: parameters: platform: @@ -1038,7 +1094,8 @@ workflows: - build_release: requires: - test - - test_updated + - check_helm + - check_compliance nightly: true context: - router @@ -1073,6 +1130,7 @@ workflows: or: - << pipeline.parameters.nightly >> - << pipeline.parameters.quick_nightly >> + - << pipeline.parameters.test_updated_cargo_deps >> jobs: - pre_verify_release: matrix: @@ -1110,16 +1168,6 @@ workflows: ignore: /.*/ tags: only: /v.*/ - - test_updated: - matrix: - parameters: - platform: - [ amd_linux_test ] - filters: - branches: - ignore: /.*/ - tags: - only: /v.*/ - test: matrix: parameters: @@ -1148,7 +1196,6 @@ workflows: - check_compliance - pre_verify_release - test - - test_updated filters: branches: ignore: /.*/ @@ -1161,6 +1208,7 @@ workflows: or: - << pipeline.parameters.nightly >> - << pipeline.parameters.quick_nightly >> + - << pipeline.parameters.test_updated_cargo_deps >> jobs: - secops/gitleaks: context: diff --git a/CHANGELOG.md b/CHANGELOG.md index bb4866bf62..bd46bf9f90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,296 @@ All notable changes to Router will be documented in this file. This project adheres to [Semantic Versioning v2.0.0](https://semver.org/spec/v2.0.0.html). +# [1.58.0] - 2024-11-27 + +> [!IMPORTANT] +> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), this release contains changes which necessarily alter the hashing algorithm used for the cache keys. On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new hashing algorithm comes into service. + +## 🚀 Features + +### Support DNS resolution strategy configuration ([PR #6109](https://github.com/apollographql/router/pull/6109)) + +The router now supports a configurable DNS resolution strategy for the URLs of coprocessors and subgraphs. +The new option is called `dns_resolution_strategy` and supports the following values: +* `ipv4_only` - Only query for `A` (IPv4) records. +* `ipv6_only` - Only query for `AAAA` (IPv6) records. +* `ipv4_and_ipv6` - Query for both `A` (IPv4) and `AAAA` (IPv6) records in parallel. +* `ipv6_then_ipv4` - Query for `AAAA` (IPv6) records first; if that fails, query for `A` (IPv4) records. +* `ipv4_then_ipv6`(default) - Query for `A` (IPv4) records first; if that fails, query for `AAAA` (IPv6) records. + +You can change the DNS resolution strategy applied to a subgraph's URL: + +```yaml title="router.yaml" +traffic_shaping: + all: + dns_resolution_strategy: ipv4_then_ipv6 + +``` + +You can also change the DNS resolution strategy applied to a coprocessor's URL: + +```yaml title="router.yaml" +coprocessor: + url: http://coprocessor.example.com:8081 + client: + dns_resolution_strategy: ipv4_then_ipv6 + +``` + +By [@IvanGoncharov](https://github.com/IvanGoncharov) in https://github.com/apollographql/router/pull/6109 + +### Configuration options for HTTP/1 max headers and buffer limits ([PR #6194](https://github.com/apollographql/router/pull/6194)) + +This update introduces configuration options that allow you to adjust the maximum number of HTTP/1 request headers and the maximum buffer size allocated for headers. + +By default, the router accepts HTTP/1 requests with up to 100 headers and allocates ~400 KiB of buffer space to store them. If you need to handle requests with more headers or require a different buffer size, you can now configure these limits in the router's configuration file: +```yaml +limits: + http1_request_max_headers: 200 + http1_request_max_buf_size: 200kib +``` + +If you are using the router as a Rust crate, the `http1_request_max_buf_size` option requires the `hyper_header_limits` feature and also necessitates using Apollo's fork of the Hyper crate until the [changes are merged upstream](https://github.com/hyperium/hyper/pull/3523). +You can include this fork by adding the following patch to your Cargo.toml file: +```toml +[patch.crates-io] +"hyper" = { git = "https://github.com/apollographql/hyper.git", tag = "header-customizations-20241108" } +``` + +By [@IvanGoncharov](https://github.com/IvanGoncharov) in https://github.com/apollographql/router/pull/6194 + +### Compress subgraph operations by generating fragments ([PR #6013](https://github.com/apollographql/router/pull/6013)) + +The router now compresses operations sent to subgraphs by default by generating fragment +definitions and using them in the operation. + +This change enables `generate_query_fragments` by default while disabling `experimental_reuse_query_fragments`. When enabled, `experimental_reuse_query_fragments` attempts to intelligently reuse the fragment definitions +from the original operation. However, fragment generation with `generate_query_fragments` is much faster and produces better outputs in most cases. + +If you are relying on the shape of fragments in your subgraph operations or tests, you can opt out of the new algorithm with the configuration below. + +> Note: The subgraph operations generated by the query planner are not guaranteed consistent release over release. We strongly recommend against relying on the shape of planned subgraph operations, as new router features and optimizations will continuously affect it. We plan to remove `experimental_reuse_query_fragments` in a future release. + +```yaml +supergraph: + generate_query_fragments: false + experimental_reuse_query_fragments: true +``` + +By [@lrlna](https://github.com/lrlna) in https://github.com/apollographql/router/pull/6013 + +### Add subgraph request id ([PR #5858](https://github.com/apollographql/router/pull/5858)) + +The router now supports a subgraph request ID that is a unique string identifying a subgraph request and response. It allows plugins and coprocessors to keep some state per subgraph request by matching on this ID. It's available in coprocessors as `subgraphRequestId` and Rhai scripts as `request.subgraph.id` and `response.subgraph.id`. + + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/5858 + +### Add `extensions.service` for all subgraph errors ([PR #6191](https://github.com/apollographql/router/pull/6191)) + + +For improved debuggability, the router now supports adding a subgraph's name as an extension to all errors originating from the subgraph. + +If `include_subgraph_errors` is `true` for a particular subgraph, all errors originating in this subgraph will have the subgraph's name exposed as a `service` extension. + +You can enable subgraph errors with the following configuration: +```yaml title="router.yaml" +include_subgraph_errors: + all: true # Propagate errors from all subgraphs +``` +> Note: This option is enabled by default by the router's [dev mode](https://www.apollographql.com/docs/graphos/reference/router/configuration#dev-mode-defaults). + +Consequently, when a subgraph returns an error, it will have a `service` extension with the subgraph name as its value. The following example shows the extension for a `products` subgraph: + +```json +{ + "data": null, + "errors": [ + { + "message": "Invalid product ID", + "path": [], + "extensions": { + "service": "products" + } + } + ] +} +``` + +By [@IvanGoncharov](https://github.com/IvanGoncharov) in https://github.com/apollographql/router/pull/6191 + +### Add `@context` support in the native query planner ([PR #6310](https://github.com/apollographql/router/pull/6310)) + +The [`@context`](https://www.apollographql.com/docs/graphos/reference/federation/directives#context) feature is now available in the [native query planner](https://www.apollographql.com/docs/graphos/routing/query-planning/native-query-planner). +This brings the native query planner to feature parity with the legacy query planner for all Federation v2 graphs. The native query planner can be enabled with the following configuration: +```yaml, filename=router.yaml +experimental_query_planner_mode: new +``` + + +By [@clenfest](https://github.com/clenfest), [@TylerBloom](https://github.com/TylerBloom) in https://github.com/apollographql/router/pull/6310 + +## 🐛 Fixes + +### Remove noisy demand control logs ([PR #6192](https://github.com/apollographql/router/pull/6192)) + +Demand control no longer logs warnings when a subgraph response is missing a requested field. + +By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/6192 + +### Renamed headers' original values can again be propagated ([PR #6281](https://github.com/apollographql/router/pull/6281)) + +[PR #4535](https://github.com/apollographql/router/pull/4535) introduced a regression where the following header propagation config would not work: + +```yaml +headers: +- propagate: + named: a + rename: b +- propagate: + named: a + rename: c +``` + +The goal of the original PR was to prevent multiple headers from being mapped to a single target header. However, it did not consider renames and instead prevented multiple mappings from the same source header. +The router now propagates headers properly and ensures that a target header is only propagated to once. + +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/6281 + +### Introspection response deduplication should always produce results ([Issue #6249](https://github.com/apollographql/router/issues/6249)) + +To reduce CPU usage, query planning and introspection queries are deduplicated. In some cases, deduplicated introspection queries were not receiving their result. This issue has been fixed, and the router now sends results in all cases. + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/6257 + +### Don't log response data upon notification failure for subgraph batching ([PR #6150](https://github.com/apollographql/router/pull/6150)) + +For a subgraph batching operation, the router now doesn't log the entire subgraph response when failing to notify a waiting batch participant. This saves the router from logging the large amount of data (PII and/or non-PII data) that a subgraph response may contain. + +By [@garypen](https://github.com/garypen) in https://github.com/apollographql/router/pull/6150 + +### Move heavy computation to a thread pool with a priority queue ([PR #6247](https://github.com/apollographql/router/pull/6247)) + +The router now avoids blocking threads when executing asynchronous code by using a thread pool with a priority queue. + +This improves the performance of the following components that can take non-trivial amounts of CPU time: + +* GraphQL parsing +* GraphQL validation +* Query planning +* Schema introspection + +The size of the thread pool is based on the number of available CPU cores. + +The thread pool replaces the router's prior implementation that used Tokio’s [`spawn_blocking`](https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html). + +`apollo.router.compute_jobs.queued` is a new gauge metric for the number of items in the thread pool's priority queue. + +> Note: when the native query planner is enabled, the dedicated queue of the legacy query planner is no longer used, so the `apollo.router.query_planning.queued` metric is no longer emitted. + +By [@SimonSapin](https://github.com/SimonSapin) in https://github.com/apollographql/router/pull/6247 + +### Limit the amount of GraphQL validation errors returned per response ([PR #6187](https://github.com/apollographql/router/pull/6187)) + +When an invalid query is submitted, the router now returns at most one hundred GraphQL parsing and validation errors in a response. This prevents generating too large of a response for a nonsensical document. + +By [@goto-bus-stop](https://github.com/goto-bus-stop) in https://github.com/apollographql/router/pull/6187 + +### Remove placeholders from file upload query variables ([PR #6293](https://github.com/apollographql/router/pull/6293)) + +Previously, file upload query variables in subgraph requests incorrectly contained internal placeholders. +According to the [GraphQL Multipart Request Spec](https://github.com/jaydenseric/graphql-multipart-request-spec?tab=readme-ov-file#multipart-form-field-structure), these variables should be set to null. +This issue has been fixed by ensuring that the router complies with the specification and improving compatibility with subgraphs handling file uploads. + +By [@IvanGoncharov](https://github.com/IvanGoncharov) in https://github.com/apollographql/router/pull/6293 + +### Overhead processing metrics should exclude subgraph response time when deduplication is enabled ([PR #6207](https://github.com/apollographql/router/pull/6207)) + +The router's calculated overhead processing time has been fixed, where the time spent waiting for the subgraph response of a deduplicated request had been incorrectly included. + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/6207 + +### Fix demand control panic for custom scalars that represent non-GraphQL-compliant JSON ([PR #6288](https://github.com/apollographql/router/pull/6288)) + +Previously, a panic could be triggered in the router's demand control plugin with the following schema: + +``` +scalar ArbitraryJson + +type MyInput { + json: ArbitraryJson +} + +type Query { + fetch(args: MyInput): Int +} +``` + +Then, submitting the query + +``` +query FetchData(: ArbitraryJson) { + fetch(args: { + json: + }) +} +``` + +and variables + +``` +{ + "myJsonValue": { + "field.with.dots": 1 + } +} +``` + +During scoring, the demand control plugin would attempt to convert the variable structure into a GraphQL-compliant structure requiring valid GraphQL names as keys. The dot characters in the keys however would cause a panic. + +With this fix, only the GraphQL compliant part of the input object is scored, and the arbitrary JSON marked by the custom scalar is scored as an opaque scalar (similar to how built-ins like `Int` or `String` are processed). + +By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/6288 + +### Fix incorrect overriding of concrete type names with interface names when merging responses ([PR #6250](https://github.com/apollographql/router/pull/6250)) + +When using `@interfaceObject`, differing pieces of data can come back with either concrete types or interface types depending on the source. Previously, receiving the data in a particular order could incorrectly result in the interface name of a type overwriting its concrete name. + +To make the response merging order-agnostic, the router now checks the schema to ensure concrete types are not overwritten with interfaces or less specific types. + +By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/6250 + +## 🛠 Maintenance + +### Query planner cache key improvements ([Issue #5160](https://github.com/apollographql/router/issues/5160)) + +Several performance improvements have been implemented for query plan cache key generation. In particular, the distributed cache's key format has changed, which adds prefixes to the different key segments to help in debugging. + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/6206 + +### Add entity caching invalidation configuration metrics ([PR #6286](https://github.com/apollographql/router/pull/6286)) + +We've added metrics for our analytics to know if entity caching invalidation is enabled. + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6286 + +### Avoid creating stub span for supergraph events if current span exists ([PR #6096](https://github.com/apollographql/router/pull/6096)) + +The router optimized its telemetry implementation by not creating a redundant span when it already has a span available to use the span's extensions for supergraph events. + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6096 + +## 📚 Documentation + +### Clarify docs for authorization directive composition ([PR #6216](https://github.com/apollographql/router/pull/6216)) + +The docs for [authorization directive composition](https://www.apollographql.com/docs/graphos/routing/security/authorization#composition-and-federation) have been clarified, including corrected code examples. + + +By [@Meschreiber](https://github.com/Meschreiber) in https://github.com/apollographql/router/pull/6216 + + + # [1.57.1] - 2024-10-31 ## 🐛 Fixes diff --git a/Cargo.lock b/Cargo.lock index c97f22a4b3..57a87514d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,11 +178,12 @@ dependencies = [ [[package]] name = "apollo-federation" -version = "1.57.1" +version = "1.58.0" dependencies = [ "apollo-compiler", "derive_more", "either", + "hashbrown 0.15.0", "hex", "indexmap 2.2.6", "insta", @@ -191,6 +192,7 @@ dependencies = [ "multimap 0.10.0", "nom", "petgraph", + "regex", "ron", "serde", "serde_json_bytes", @@ -229,7 +231,7 @@ dependencies = [ [[package]] name = "apollo-router" -version = "1.57.1" +version = "1.58.0" dependencies = [ "access-json", "ahash", @@ -397,7 +399,7 @@ dependencies = [ [[package]] name = "apollo-router-benchmarks" -version = "1.57.1" +version = "1.58.0" dependencies = [ "apollo-parser", "apollo-router", @@ -413,7 +415,7 @@ dependencies = [ [[package]] name = "apollo-router-scaffold" -version = "1.57.1" +version = "1.58.0" dependencies = [ "anyhow", "cargo-scaffold", @@ -2577,6 +2579,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "forbid-anonymous-operations" version = "0.1.0" @@ -2927,8 +2935,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -3085,6 +3093,17 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hdrhistogram" version = "7.5.4" @@ -3364,9 +3383,8 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +version = "0.14.31" +source = "git+https://github.com/apollographql/hyper.git?tag=header-customizations-20241108#c42aec785394b40645a283384838b856beace011" dependencies = [ "bytes", "futures-channel", @@ -3379,6 +3397,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "smallvec", "socket2 0.5.7", "tokio", "tower-service", @@ -5288,14 +5307,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -5309,13 +5328,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -5332,9 +5351,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" diff --git a/Cargo.toml b/Cargo.toml index d8514547c6..c492b05480 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,3 +76,6 @@ sha1 = "0.10.6" tempfile = "3.10.1" tokio = { version = "1.36.0", features = ["full"] } tower = { version = "0.4.13", features = ["full"] } + +[patch.crates-io] +"hyper" = { git = "https://github.com/apollographql/hyper.git", tag = "header-customizations-20241108" } diff --git a/README.md b/README.md index 0f87d6d343..df19eb7593 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,6 @@ --- -**Announcement:** -Join 1000+ engineers at GraphQL Summit for talks, workshops, and office hours, Oct 8-10 in NYC. [Get your pass here ->](https://summit.graphql.com/?utm_campaign=github_federation_readme) - ---- - # Apollo Router Core The **Apollo Router Core** is a configurable, high-performance **graph router** written in Rust to run a [federated supergraph](https://www.apollographql.com/docs/federation/) that uses [Apollo Federation 2](https://www.apollographql.com/docs/federation/v2/federation-2/new-in-federation-2). diff --git a/about.toml b/about.toml index dbd148a7a4..094647afae 100644 --- a/about.toml +++ b/about.toml @@ -9,7 +9,8 @@ accepted = [ "LicenseRef-ring", "MIT", "MPL-2.0", - "Unicode-DFS-2016" + "Unicode-DFS-2016", + "Zlib" ] # See https://github.com/EmbarkStudios/cargo-about/pull/216 diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml index ce77a6af9e..c0103f435e 100644 --- a/apollo-federation/Cargo.toml +++ b/apollo-federation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-federation" -version = "1.57.1" +version = "1.58.0" authors = ["The Apollo GraphQL Contributors"] edition = "2021" description = "Apollo Federation" @@ -22,6 +22,7 @@ time = { version = "0.3.34", default-features = false, features = [ "local-offset", ] } derive_more = "0.99.17" +hashbrown = "0.15.0" indexmap = { version = "2.2.6", features = ["serde"] } itertools = "0.13.0" lazy_static = "1.4.0" @@ -37,6 +38,7 @@ url = "2" tracing = "0.1.40" ron = { version = "0.8.1", optional = true } either = "1.13.0" +regex = "1.11.1" [dev-dependencies] hex.workspace = true diff --git a/apollo-federation/src/compat.rs b/apollo-federation/src/compat.rs index bd571f3f5c..d3d8caa537 100644 --- a/apollo-federation/src/compat.rs +++ b/apollo-federation/src/compat.rs @@ -207,6 +207,7 @@ fn coerce_value( // Custom scalars accept any value, even objects and lists. (Value::Object(_), Some(ExtendedType::Scalar(scalar))) if !scalar.is_built_in() => {} (Value::List(_), Some(ExtendedType::Scalar(scalar))) if !scalar.is_built_in() => {} + (Value::Enum(_), Some(ExtendedType::Scalar(scalar))) if !scalar.is_built_in() => {} // Enums must match the type. (Value::Enum(value), Some(ExtendedType::Enum(enum_))) if enum_.values.contains_key(value) => {} @@ -364,6 +365,11 @@ pub(crate) fn coerce_executable_values(schema: &Valid, document: &mut Ex for operation in document.operations.named.values_mut() { coerce_operation_values(schema, operation); } + for fragment in document.fragments.values_mut() { + let fragment = fragment.make_mut(); + coerce_directive_application_values(schema, &mut fragment.directives); + coerce_selection_set_values(schema, &mut fragment.selection_set); + } } /// Applies default value coercion and removes non-semantic directives so that @@ -415,4 +421,72 @@ mod tests { } "#); } + + #[test] + fn coerces_enum_values() { + let schema = Schema::parse_and_validate( + r#" + scalar CustomScalar + type Query { + test( + string: String!, + strings: [String!]!, + custom: CustomScalar!, + customList: [CustomScalar!]!, + ): Int + } + "#, + "schema.graphql", + ) + .unwrap(); + + // Enum literals are only coerced into lists if the item type is a custom scalar type. + insta::assert_snapshot!(parse_and_coerce(&schema, r#" + { + test(string: enumVal1, strings: enumVal2, custom: enumVal1, customList: enumVal2) + } + "#), @r###" + { + test(string: enumVal1, strings: enumVal2, custom: enumVal1, customList: [enumVal2]) + } + "###); + } + + #[test] + fn coerces_in_fragment_definitions() { + let schema = Schema::parse_and_validate( + r#" + type T { + get(bools: [Boolean!]!): Int + } + type Query { + test: T + } + "#, + "schema.graphql", + ) + .unwrap(); + + insta::assert_snapshot!(parse_and_coerce(&schema, r#" + { + test { + ...f + } + } + + fragment f on T { + get(bools: true) + } + "#), @r###" + { + test { + ...f + } + } + + fragment f on T { + get(bools: [true]) + } + "###); + } } diff --git a/apollo-federation/src/error/mod.rs b/apollo-federation/src/error/mod.rs index 97b7d2757d..8d1ceb156b 100644 --- a/apollo-federation/src/error/mod.rs +++ b/apollo-federation/src/error/mod.rs @@ -13,11 +13,44 @@ use lazy_static::lazy_static; use crate::subgraph::spec::FederationSpecError; -/// Break out of the current function, returning an internal error. +/// Create an internal error. +/// +/// # Example +/// ```rust +/// use apollo_federation::internal_error; +/// use apollo_federation::error::FederationError; +/// # fn may_be_none() -> Option<()> { None } +/// +/// const NAME: &str = "the thing"; +/// let result: Result<(), FederationError> = may_be_none() +/// .ok_or_else(|| internal_error!("Expected {NAME} to be Some")); +/// ``` #[macro_export] macro_rules! internal_error { ( $( $arg:tt )+ ) => { - return Err($crate::error::FederationError::internal(format!( $( $arg )+ )).into()); + $crate::error::FederationError::internal(format!( $( $arg )+ )) + } +} + +/// Break out of the current function, returning an internal error. +/// +/// # Example +/// ```rust +/// use apollo_federation::bail; +/// use apollo_federation::error::FederationError; +/// # fn may_be_none() -> Option<()> { None } +/// +/// fn example() -> Result<(), FederationError> { +/// bail!("Something went horribly wrong"); +/// unreachable!() +/// } +/// # +/// # _ = example(); +/// ``` +#[macro_export] +macro_rules! bail { + ( $( $arg:tt )+ ) => { + return Err($crate::internal_error!( $( $arg )+ ).into()) } } @@ -26,6 +59,18 @@ macro_rules! internal_error { /// /// Treat this as an assertion. It must only be used for conditions that *should never happen* /// in normal operation. +/// +/// # Example +/// ```rust,no_run +/// use apollo_federation::ensure; +/// use apollo_federation::error::FederationError; +/// # fn may_be_none() -> Option<()> { None } +/// +/// fn example() -> Result<(), FederationError> { +/// ensure!(1 == 0, "Something went horribly wrong"); +/// unreachable!() +/// } +/// ``` #[macro_export] macro_rules! ensure { ( $expr:expr, $( $arg:tt )+ ) => { diff --git a/apollo-federation/src/link/argument.rs b/apollo-federation/src/link/argument.rs index 12702dadb2..aeac4ccd0e 100644 --- a/apollo-federation/src/link/argument.rs +++ b/apollo-federation/src/link/argument.rs @@ -5,6 +5,7 @@ use apollo_compiler::schema::Directive; use apollo_compiler::Name; use apollo_compiler::Node; +use crate::bail; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::link::graphql_definition::BooleanOrVariable; @@ -132,3 +133,20 @@ pub(crate) fn directive_optional_variable_boolean_argument( None => Ok(None), } } + +pub(crate) fn directive_optional_list_argument<'a>( + application: &'a Node, + name: &'_ Name, +) -> Result]>, FederationError> { + match application.specified_argument_by_name(name) { + None => Ok(None), + Some(value) => match value.as_ref() { + Value::Null => Ok(None), + Value::List(values) => Ok(Some(values.as_slice())), + _ => bail!( + r#"Argument "{name}" of directive "@{}" must be a boolean."#, + application.name + ), + }, + } +} diff --git a/apollo-federation/src/link/context_spec_definition.rs b/apollo-federation/src/link/context_spec_definition.rs new file mode 100644 index 0000000000..ff4217c2d6 --- /dev/null +++ b/apollo-federation/src/link/context_spec_definition.rs @@ -0,0 +1,62 @@ +use apollo_compiler::name; +use apollo_compiler::Name; +use lazy_static::lazy_static; + +use crate::error::FederationError; +use crate::link::spec::Identity; +use crate::link::spec::Url; +use crate::link::spec::Version; +use crate::link::spec_definition::SpecDefinition; +use crate::link::spec_definition::SpecDefinitions; +use crate::schema::FederationSchema; + +pub(crate) const CONTEXT_DIRECTIVE_NAME_IN_SPEC: Name = name!("context"); +pub(crate) const CONTEXT_DIRECTIVE_NAME_DEFAULT: Name = name!("federation__context"); + +#[derive(Clone)] +pub(crate) struct ContextSpecDefinition { + url: Url, + minimum_federation_version: Option, +} + +impl ContextSpecDefinition { + pub(crate) fn new(version: Version, minimum_federation_version: Option) -> Self { + Self { + url: Url { + identity: Identity::context_identity(), + version, + }, + minimum_federation_version, + } + } + + pub(crate) fn context_directive_name_in_schema( + &self, + schema: &FederationSchema, + ) -> Result { + Ok(self + .directive_name_in_schema(schema, &CONTEXT_DIRECTIVE_NAME_IN_SPEC)? + .unwrap_or(CONTEXT_DIRECTIVE_NAME_DEFAULT)) + } +} + +impl SpecDefinition for ContextSpecDefinition { + fn url(&self) -> &Url { + &self.url + } + + fn minimum_federation_version(&self) -> Option<&Version> { + self.minimum_federation_version.as_ref() + } +} + +lazy_static! { + pub(crate) static ref CONTEXT_VERSIONS: SpecDefinitions = { + let mut definitions = SpecDefinitions::new(Identity::context_identity()); + definitions.add(ContextSpecDefinition::new( + Version { major: 0, minor: 1 }, + Some(Version { major: 2, minor: 8 }), + )); + definitions + }; +} diff --git a/apollo-federation/src/link/federation_spec_definition.rs b/apollo-federation/src/link/federation_spec_definition.rs index 184e93b690..a773d860c7 100644 --- a/apollo-federation/src/link/federation_spec_definition.rs +++ b/apollo-federation/src/link/federation_spec_definition.rs @@ -32,12 +32,16 @@ pub(crate) const FEDERATION_REQUIRES_DIRECTIVE_NAME_IN_SPEC: Name = name!("requi pub(crate) const FEDERATION_PROVIDES_DIRECTIVE_NAME_IN_SPEC: Name = name!("provides"); pub(crate) const FEDERATION_SHAREABLE_DIRECTIVE_NAME_IN_SPEC: Name = name!("shareable"); pub(crate) const FEDERATION_OVERRIDE_DIRECTIVE_NAME_IN_SPEC: Name = name!("override"); +pub(crate) const FEDERATION_CONTEXT_DIRECTIVE_NAME_IN_SPEC: Name = name!("context"); +pub(crate) const FEDERATION_FROM_CONTEXT_DIRECTIVE_NAME_IN_SPEC: Name = name!("fromContext"); pub(crate) const FEDERATION_FIELDS_ARGUMENT_NAME: Name = name!("fields"); pub(crate) const FEDERATION_RESOLVABLE_ARGUMENT_NAME: Name = name!("resolvable"); pub(crate) const FEDERATION_REASON_ARGUMENT_NAME: Name = name!("reason"); pub(crate) const FEDERATION_FROM_ARGUMENT_NAME: Name = name!("from"); pub(crate) const FEDERATION_OVERRIDE_LABEL_ARGUMENT_NAME: Name = name!("label"); +pub(crate) const FEDERATION_NAME_ARGUMENT_NAME: Name = name!("name"); +pub(crate) const FEDERATION_FIELD_ARGUMENT_NAME: Name = name!("field"); pub(crate) struct KeyDirectiveArguments<'doc> { pub(crate) fields: &'doc str, @@ -52,6 +56,14 @@ pub(crate) struct ProvidesDirectiveArguments<'doc> { pub(crate) fields: &'doc str, } +pub(crate) struct ContextDirectiveArguments<'doc> { + pub(crate) name: &'doc str, +} + +pub(crate) struct FromContextDirectiveArguments<'doc> { + pub(crate) field: &'doc str, +} + pub(crate) struct OverrideDirectiveArguments<'doc> { pub(crate) from: &'doc str, pub(crate) label: Option<&'doc str>, @@ -422,6 +434,111 @@ impl FederationSpecDefinition { }) } + pub(crate) fn context_directive_definition<'schema>( + &self, + schema: &'schema FederationSchema, + ) -> Result<&'schema Node, FederationError> { + self.directive_definition(schema, &FEDERATION_CONTEXT_DIRECTIVE_NAME_IN_SPEC)? + .ok_or_else(|| { + FederationError::internal(format!( + "Unexpectedly could not find federation spec's \"@{}\" directive definition", + FEDERATION_CONTEXT_DIRECTIVE_NAME_IN_SPEC, + )) + }) + } + + pub(crate) fn context_directive( + &self, + schema: &FederationSchema, + name: String, + ) -> Result { + let name_in_schema = self + .directive_name_in_schema(schema, &FEDERATION_CONTEXT_DIRECTIVE_NAME_IN_SPEC)? + .ok_or_else(|| SingleFederationError::Internal { + message: "Unexpectedly could not find federation spec in schema".to_owned(), + })?; + + let arguments = vec![Node::new(Argument { + name: FEDERATION_NAME_ARGUMENT_NAME, + value: Node::new(Value::String(name)), + })]; + + Ok(Directive { + name: name_in_schema, + arguments, + }) + } + + pub(crate) fn context_directive_arguments( + application: &Node, + ) -> Result { + Ok(ContextDirectiveArguments { + name: directive_required_string_argument(application, &FEDERATION_NAME_ARGUMENT_NAME)?, + }) + } + + // The directive is named `@fromContex`. This is confusing for clippy, as + // `from` is a conventional prefix used in conversion methods, which do not + // take `self` as an argument. This function does **not** perform + // conversion, but extracts `@fromContext` directive definition. + #[allow(clippy::wrong_self_convention)] + pub(crate) fn from_context_directive_definition<'schema>( + &self, + schema: &'schema FederationSchema, + ) -> Result<&'schema Node, FederationError> { + self.directive_definition(schema, &FEDERATION_FROM_CONTEXT_DIRECTIVE_NAME_IN_SPEC)? + .ok_or_else(|| { + FederationError::internal(format!( + "Unexpectedly could not find federation spec's \"@{}\" directive definition", + FEDERATION_FROM_CONTEXT_DIRECTIVE_NAME_IN_SPEC, + )) + }) + } + + // The directive is named `@fromContex`. This is confusing for clippy, as + // `from` is a conventional prefix used in conversion methods, which do not + // take `self` as an argument. This function does **not** perform + // conversion, but extracts `@fromContext` directive arguments. + #[allow(clippy::wrong_self_convention)] + pub(crate) fn from_context_directive_arguments<'doc>( + &self, + application: &'doc Node, + ) -> Result, FederationError> { + Ok(FromContextDirectiveArguments { + field: directive_required_string_argument( + application, + &FEDERATION_FIELD_ARGUMENT_NAME, + )?, + }) + } + + // The directive is named `@fromContex`. This is confusing for clippy, as + // `from` is a conventional prefix used in conversion methods, which do not + // take `self` as an argument. This function does **not** perform + // conversion, but extracts `@fromContext` directive. + #[allow(clippy::wrong_self_convention)] + pub(crate) fn from_context_directive( + &self, + schema: &FederationSchema, + name: String, + ) -> Result { + let name_in_schema = self + .directive_name_in_schema(schema, &FEDERATION_FROM_CONTEXT_DIRECTIVE_NAME_IN_SPEC)? + .ok_or_else(|| SingleFederationError::Internal { + message: "Unexpectedly could not find federation spec in schema".to_owned(), + })?; + + let arguments = vec![Node::new(Argument { + name: FEDERATION_FIELD_ARGUMENT_NAME, + value: Node::new(Value::String(name)), + })]; + + Ok(Directive { + name: name_in_schema, + arguments, + }) + } + pub(crate) fn get_cost_spec_definition( &self, schema: &FederationSchema, diff --git a/apollo-federation/src/link/join_spec_definition.rs b/apollo-federation/src/link/join_spec_definition.rs index 1328cbdcb7..19124f579a 100644 --- a/apollo-federation/src/link/join_spec_definition.rs +++ b/apollo-federation/src/link/join_spec_definition.rs @@ -1,3 +1,4 @@ +use apollo_compiler::ast::Value; use apollo_compiler::name; use apollo_compiler::schema::Directive; use apollo_compiler::schema::DirectiveDefinition; @@ -5,8 +6,11 @@ use apollo_compiler::schema::EnumType; use apollo_compiler::schema::ExtendedType; use apollo_compiler::Name; use apollo_compiler::Node; +use itertools::Itertools; use lazy_static::lazy_static; +use super::argument::directive_optional_list_argument; +use crate::bail; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::link::argument::directive_optional_boolean_argument; @@ -45,6 +49,7 @@ pub(crate) const JOIN_OVERRIDE_LABEL_ARGUMENT_NAME: Name = name!("overrideLabel" pub(crate) const JOIN_USEROVERRIDDEN_ARGUMENT_NAME: Name = name!("usedOverridden"); pub(crate) const JOIN_INTERFACE_ARGUMENT_NAME: Name = name!("interface"); pub(crate) const JOIN_MEMBER_ARGUMENT_NAME: Name = name!("member"); +pub(crate) const JOIN_MEMBER_CONTEXT_ARGUMENTS: Name = name!("contextArguments"); pub(crate) struct GraphDirectiveArguments<'doc> { pub(crate) name: &'doc str, @@ -59,6 +64,78 @@ pub(crate) struct TypeDirectiveArguments<'doc> { pub(crate) is_interface_object: bool, } +pub(crate) struct ContextArgument<'doc> { + pub(crate) name: &'doc str, + pub(crate) type_: &'doc str, + pub(crate) context: &'doc str, + pub(crate) selection: &'doc str, +} + +impl<'doc> TryFrom<&'doc Value> for ContextArgument<'doc> { + type Error = FederationError; + + fn try_from(value: &'doc Value) -> Result { + fn insert_value<'a>( + name: &str, + field: &mut Option<&'a Value>, + value: &'a Value, + ) -> Result<(), FederationError> { + if let Some(first_value) = field { + bail!("Duplicate contextArgument for '{name}' field: {first_value} and {value}") + } + let _ = field.insert(value); + Ok(()) + } + + fn field_or_else<'a>( + field_name: &'static str, + field: Option<&'a Value>, + ) -> Result<&'a str, FederationError> { + field + .ok_or_else(|| { + FederationError::internal(format!( + "'{field_name}' field was missing from contextArgument" + )) + })? + .as_str() + .ok_or_else(|| { + FederationError::internal(format!( + "'{field_name}' field of contextArgument was not a string" + )) + }) + } + + let Value::Object(names) = value else { + bail!("Item in contextArgument list is not an object {value}") + }; + let mut name = None; + let mut type_ = None; + let mut context = None; + let mut selection = None; + for (arg_name, value) in names.as_slice() { + match arg_name.as_str() { + "name" => insert_value(arg_name, &mut name, value)?, + "type" => insert_value(arg_name, &mut type_, value)?, + "context" => insert_value(arg_name, &mut context, value)?, + "selection" => insert_value(arg_name, &mut selection, value)?, + _ => bail!("Found unknown contextArgument {arg_name}"), + } + } + + let name = field_or_else("name", name)?; + let type_ = field_or_else("type_", type_)?; + let context = field_or_else("context", context)?; + let selection = field_or_else("selection", selection)?; + + Ok(Self { + name, + type_, + context, + selection, + }) + } +} + pub(crate) struct FieldDirectiveArguments<'doc> { pub(crate) graph: Option, pub(crate) requires: Option<&'doc str>, @@ -68,6 +145,7 @@ pub(crate) struct FieldDirectiveArguments<'doc> { pub(crate) override_: Option<&'doc str>, pub(crate) override_label: Option<&'doc str>, pub(crate) user_overridden: Option, + pub(crate) context_arguments: Option>>, } pub(crate) struct ImplementsDirectiveArguments<'doc> { @@ -228,6 +306,17 @@ impl JoinSpecDefinition { application, &JOIN_USEROVERRIDDEN_ARGUMENT_NAME, )?, + context_arguments: directive_optional_list_argument( + application, + &JOIN_MEMBER_CONTEXT_ARGUMENTS, + )? + .map(|values| { + values + .iter() + .map(|value| ContextArgument::try_from(value.as_ref())) + .try_collect() + }) + .transpose()?, }) } diff --git a/apollo-federation/src/link/mod.rs b/apollo-federation/src/link/mod.rs index 76a59da2ea..39f51e9499 100644 --- a/apollo-federation/src/link/mod.rs +++ b/apollo-federation/src/link/mod.rs @@ -22,6 +22,7 @@ use crate::link::spec::Identity; use crate::link::spec::Url; pub(crate) mod argument; +pub(crate) mod context_spec_definition; pub(crate) mod cost_spec_definition; pub mod database; pub(crate) mod federation_spec_definition; diff --git a/apollo-federation/src/link/spec.rs b/apollo-federation/src/link/spec.rs index 5c1386644b..78fe3f33e9 100644 --- a/apollo-federation/src/link/spec.rs +++ b/apollo-federation/src/link/spec.rs @@ -95,6 +95,13 @@ impl Identity { name: name!("cost"), } } + + pub fn context_identity() -> Identity { + Identity { + domain: APOLLO_SPEC_DOMAIN.to_string(), + name: name!("context"), + } + } } /// The version of a `@link` specification, in the form of a major and minor version numbers. diff --git a/apollo-federation/src/operation/contains.rs b/apollo-federation/src/operation/contains.rs index b734dd304d..904fcdb9d0 100644 --- a/apollo-federation/src/operation/contains.rs +++ b/apollo-federation/src/operation/contains.rs @@ -163,15 +163,15 @@ impl SelectionSet { let mut is_equal = true; let mut did_ignore_typename = false; - for (key, other_selection) in other.selections.iter() { - if key.is_typename_field() && options.ignore_missing_typename { + for other_selection in other.selections.values() { + if other_selection.is_typename_field() && options.ignore_missing_typename { if !self.has_top_level_typename_field() { did_ignore_typename = true; } continue; } - let Some(self_selection) = self.selections.get(key) else { + let Some(self_selection) = self.selections.get(other_selection.key()) else { return Containment::NotContained; }; diff --git a/apollo-federation/src/operation/merging.rs b/apollo-federation/src/operation/merging.rs index c56ff5e66e..6b8e89193c 100644 --- a/apollo-federation/src/operation/merging.rs +++ b/apollo-federation/src/operation/merging.rs @@ -15,9 +15,9 @@ use super::NamedFragments; use super::Selection; use super::SelectionSet; use super::SelectionValue; +use crate::bail; use crate::ensure; use crate::error::FederationError; -use crate::internal_error; impl<'a> FieldSelectionValue<'a> { /// Merges the given field selections into this one. @@ -50,14 +50,14 @@ impl<'a> FieldSelectionValue<'a> { ); if self.get().selection_set.is_some() { let Some(other_selection_set) = &other.selection_set else { - internal_error!( + bail!( "Field \"{}\" has composite type but not a selection set", other_field.field_position, ); }; selection_sets.push(other_selection_set); } else if other.selection_set.is_some() { - internal_error!( + bail!( "Field \"{}\" has non-composite type but also has a selection set", other_field.field_position, ); @@ -183,17 +183,17 @@ impl SelectionSet { let target = Arc::make_mut(&mut self.selections); for other_selection in others { let other_key = other_selection.key(); - match target.entry(other_key.clone()) { + match target.entry(other_key) { selection_map::Entry::Occupied(existing) => match existing.get() { Selection::Field(self_field_selection) => { let Selection::Field(other_field_selection) = other_selection else { - internal_error!( + bail!( "Field selection key for field \"{}\" references non-field selection", self_field_selection.field.field_position, ); }; fields - .entry(other_key) + .entry(other_key.to_owned_key()) .or_insert_with(Vec::new) .push(other_field_selection); } @@ -201,13 +201,13 @@ impl SelectionSet { let Selection::FragmentSpread(other_fragment_spread_selection) = other_selection else { - internal_error!( + bail!( "Fragment spread selection key for fragment \"{}\" references non-field selection", self_fragment_spread_selection.spread.fragment_name, ); }; fragment_spreads - .entry(other_key) + .entry(other_key.to_owned_key()) .or_insert_with(Vec::new) .push(other_fragment_spread_selection); } @@ -215,7 +215,7 @@ impl SelectionSet { let Selection::InlineFragment(other_inline_fragment_selection) = other_selection else { - internal_error!( + bail!( "Inline fragment selection key under parent type \"{}\" {}references non-field selection", self_inline_fragment_selection.inline_fragment.parent_type_position, self_inline_fragment_selection.inline_fragment.type_condition_position.clone() @@ -226,7 +226,7 @@ impl SelectionSet { ); }; inline_fragments - .entry(other_key) + .entry(other_key.to_owned_key()) .or_insert_with(Vec::new) .push(other_inline_fragment_selection); } @@ -237,10 +237,11 @@ impl SelectionSet { } } - for (key, self_selection) in target.iter_mut() { + for self_selection in target.values_mut() { + let key = self_selection.key().to_owned_key(); match self_selection { SelectionValue::Field(mut self_field_selection) => { - if let Some(other_field_selections) = fields.shift_remove(key) { + if let Some(other_field_selections) = fields.shift_remove(&key) { self_field_selection.merge_into( other_field_selections.iter().map(|selection| &***selection), )?; @@ -248,7 +249,7 @@ impl SelectionSet { } SelectionValue::FragmentSpread(mut self_fragment_spread_selection) => { if let Some(other_fragment_spread_selections) = - fragment_spreads.shift_remove(key) + fragment_spreads.shift_remove(&key) { self_fragment_spread_selection.merge_into( other_fragment_spread_selections @@ -259,7 +260,7 @@ impl SelectionSet { } SelectionValue::InlineFragment(mut self_inline_fragment_selection) => { if let Some(other_inline_fragment_selections) = - inline_fragments.shift_remove(key) + inline_fragments.shift_remove(&key) { self_inline_fragment_selection.merge_into( other_inline_fragment_selections @@ -368,7 +369,7 @@ pub(crate) fn merge_selection_sets( mut selection_sets: Vec, ) -> Result { let Some((first, remainder)) = selection_sets.split_first_mut() else { - internal_error!("merge_selection_sets(): must have at least one selection set"); + bail!("merge_selection_sets(): must have at least one selection set"); }; first.merge_into(remainder.iter())?; diff --git a/apollo-federation/src/operation/mod.rs b/apollo-federation/src/operation/mod.rs index 2eaacfd4e4..1fe1f16287 100644 --- a/apollo-federation/src/operation/mod.rs +++ b/apollo-federation/src/operation/mod.rs @@ -20,7 +20,6 @@ use std::ops::Deref; use std::sync::atomic; use std::sync::Arc; -use apollo_compiler::collections::HashSet; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::executable; @@ -74,7 +73,7 @@ static NEXT_ID: atomic::AtomicUsize = atomic::AtomicUsize::new(1); /// /// Note that we shouldn't add `derive(Serialize, Deserialize)` to this without changing the types /// to be something like UUIDs. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] // NOTE(@TylerBloom): This feature gate can be removed once the condition in the comment above is // met. Note that there are `serde(skip)` statements that should be removed once this is removed. #[cfg_attr(feature = "snapshot_tracing", derive(Serialize))] @@ -259,434 +258,16 @@ impl PartialEq for SelectionSet { impl Eq for SelectionSet {} -mod selection_map { - use std::borrow::Cow; - use std::iter::Map; - use std::ops::Deref; - use std::sync::Arc; - - use apollo_compiler::collections::IndexMap; - use serde::Serialize; - - use crate::error::FederationError; - use crate::error::SingleFederationError::Internal; - use crate::operation::field_selection::FieldSelection; - use crate::operation::fragment_spread_selection::FragmentSpreadSelection; - use crate::operation::inline_fragment_selection::InlineFragmentSelection; - use crate::operation::HasSelectionKey; - use crate::operation::Selection; - use crate::operation::SelectionKey; - use crate::operation::SelectionSet; - use crate::operation::SiblingTypename; - - /// A "normalized" selection map is an optimized representation of a selection set which does - /// not contain selections with the same selection "key". Selections that do have the same key - /// are merged during the normalization process. By storing a selection set as a map, we can - /// efficiently merge/join multiple selection sets. - /// - /// Because the key depends strictly on the value, we expose the underlying map's API in a - /// read-only capacity, while mutations use an API closer to `IndexSet`. We don't just use an - /// `IndexSet` since key computation is expensive (it involves sorting). This type is in its own - /// module to prevent code from accidentally mutating the underlying map outside the mutation - /// API. - #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] - pub(crate) struct SelectionMap(IndexMap); - - impl Deref for SelectionMap { - type Target = IndexMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl SelectionMap { - pub(crate) fn new() -> Self { - SelectionMap(IndexMap::default()) - } - - #[cfg(test)] - pub(crate) fn clear(&mut self) { - self.0.clear(); - } - - pub(crate) fn insert(&mut self, value: Selection) -> Option { - self.0.insert(value.key(), value) - } - - /// Remove a selection from the map. Returns the selection and its numeric index. - pub(crate) fn remove(&mut self, key: &SelectionKey) -> Option<(usize, Selection)> { - // We specifically use shift_remove() instead of swap_remove() to maintain order. - self.0 - .shift_remove_full(key) - .map(|(index, _key, selection)| (index, selection)) - } - - pub(crate) fn retain( - &mut self, - mut predicate: impl FnMut(&SelectionKey, &Selection) -> bool, - ) { - self.0.retain(|k, v| predicate(k, v)) - } - - pub(crate) fn get_mut(&mut self, key: &SelectionKey) -> Option { - self.0.get_mut(key).map(SelectionValue::new) - } - - pub(crate) fn iter_mut(&mut self) -> IterMut { - self.0.iter_mut().map(|(k, v)| (k, SelectionValue::new(v))) - } - - pub(super) fn entry(&mut self, key: SelectionKey) -> Entry { - match self.0.entry(key) { - indexmap::map::Entry::Occupied(entry) => Entry::Occupied(OccupiedEntry(entry)), - indexmap::map::Entry::Vacant(entry) => Entry::Vacant(VacantEntry(entry)), - } - } - - pub(crate) fn extend(&mut self, other: SelectionMap) { - self.0.extend(other.0) - } - - pub(crate) fn extend_ref(&mut self, other: &SelectionMap) { - self.0 - .extend(other.iter().map(|(k, v)| (k.clone(), v.clone()))) - } - - /// Returns the selection set resulting from "recursively" filtering any selection - /// that does not match the provided predicate. - /// This method calls `predicate` on every selection of the selection set, - /// not just top-level ones, and apply a "depth-first" strategy: - /// when the predicate is called on a given selection it is guaranteed that - /// filtering has happened on all the selections of its sub-selection. - pub(crate) fn filter_recursive_depth_first( - &self, - predicate: &mut dyn FnMut(&Selection) -> Result, - ) -> Result, FederationError> { - fn recur_sub_selections<'sel>( - selection: &'sel Selection, - predicate: &mut dyn FnMut(&Selection) -> Result, - ) -> Result, FederationError> { - Ok(match selection { - Selection::Field(field) => { - if let Some(sub_selections) = &field.selection_set { - match sub_selections.filter_recursive_depth_first(predicate)? { - Cow::Borrowed(_) => Cow::Borrowed(selection), - Cow::Owned(new) => Cow::Owned(Selection::from_field( - field.field.clone(), - Some(new), - )), - } - } else { - Cow::Borrowed(selection) - } - } - Selection::InlineFragment(fragment) => match fragment - .selection_set - .filter_recursive_depth_first(predicate)? - { - Cow::Borrowed(_) => Cow::Borrowed(selection), - Cow::Owned(selection_set) => Cow::Owned(Selection::InlineFragment( - Arc::new(InlineFragmentSelection::new( - fragment.inline_fragment.clone(), - selection_set, - )), - )), - }, - Selection::FragmentSpread(_) => { - return Err(FederationError::internal("unexpected fragment spread")) - } - }) - } - let mut iter = self.0.iter(); - let mut enumerated = (&mut iter).enumerate(); - let mut new_map: IndexMap<_, _>; - loop { - let Some((index, (key, selection))) = enumerated.next() else { - return Ok(Cow::Borrowed(self)); - }; - let filtered = recur_sub_selections(selection, predicate)?; - let keep = predicate(&filtered)?; - if keep && matches!(filtered, Cow::Borrowed(_)) { - // Nothing changed so far, continue without cloning - continue; - } - - // Clone the map so far - new_map = self.0.as_slice()[..index] - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - - if keep { - new_map.insert(key.clone(), filtered.into_owned()); - } - break; - } - for (key, selection) in iter { - let filtered = recur_sub_selections(selection, predicate)?; - if predicate(&filtered)? { - new_map.insert(key.clone(), filtered.into_owned()); - } - } - Ok(Cow::Owned(Self(new_map))) - } - } - - impl FromIterator for SelectionMap - where - A: Into, - { - fn from_iter>(iter: T) -> Self { - let mut map = Self::new(); - for selection in iter { - map.insert(selection.into()); - } - map - } - } - - type IterMut<'a> = Map< - indexmap::map::IterMut<'a, SelectionKey, Selection>, - fn((&'a SelectionKey, &'a mut Selection)) -> (&'a SelectionKey, SelectionValue<'a>), - >; - - /// A mutable reference to a `Selection` value in a `SelectionMap`, which - /// also disallows changing key-related data (to maintain the invariant that a value's key is - /// the same as it's map entry's key). - #[derive(Debug)] - pub(crate) enum SelectionValue<'a> { - Field(FieldSelectionValue<'a>), - FragmentSpread(FragmentSpreadSelectionValue<'a>), - InlineFragment(InlineFragmentSelectionValue<'a>), - } - - impl<'a> SelectionValue<'a> { - pub(crate) fn new(selection: &'a mut Selection) -> Self { - match selection { - Selection::Field(field_selection) => { - SelectionValue::Field(FieldSelectionValue::new(field_selection)) - } - Selection::FragmentSpread(fragment_spread_selection) => { - SelectionValue::FragmentSpread(FragmentSpreadSelectionValue::new( - fragment_spread_selection, - )) - } - Selection::InlineFragment(inline_fragment_selection) => { - SelectionValue::InlineFragment(InlineFragmentSelectionValue::new( - inline_fragment_selection, - )) - } - } - } - - #[cfg(test)] - pub(super) fn get_selection_set_mut(&mut self) -> Option<&mut SelectionSet> { - match self { - Self::Field(field) => field.get_selection_set_mut().as_mut(), - Self::FragmentSpread(spread) => Some(spread.get_selection_set_mut()), - Self::InlineFragment(inline) => Some(inline.get_selection_set_mut()), - } - } - } - - #[derive(Debug)] - pub(crate) struct FieldSelectionValue<'a>(&'a mut Arc); - - impl<'a> FieldSelectionValue<'a> { - pub(crate) fn new(field_selection: &'a mut Arc) -> Self { - Self(field_selection) - } - - pub(crate) fn get(&self) -> &Arc { - self.0 - } - - pub(crate) fn get_sibling_typename_mut(&mut self) -> &mut Option { - Arc::make_mut(self.0).field.sibling_typename_mut() - } - - pub(crate) fn get_selection_set_mut(&mut self) -> &mut Option { - &mut Arc::make_mut(self.0).selection_set - } - } - - #[derive(Debug)] - pub(crate) struct FragmentSpreadSelectionValue<'a>(&'a mut Arc); - - impl<'a> FragmentSpreadSelectionValue<'a> { - pub(crate) fn new(fragment_spread_selection: &'a mut Arc) -> Self { - Self(fragment_spread_selection) - } - - #[cfg(test)] - pub(crate) fn get_selection_set_mut(&mut self) -> &mut SelectionSet { - &mut Arc::make_mut(self.0).selection_set - } - - pub(crate) fn get(&self) -> &Arc { - self.0 - } - } - - #[derive(Debug)] - pub(crate) struct InlineFragmentSelectionValue<'a>(&'a mut Arc); - - impl<'a> InlineFragmentSelectionValue<'a> { - pub(crate) fn new(inline_fragment_selection: &'a mut Arc) -> Self { - Self(inline_fragment_selection) - } - - pub(crate) fn get(&self) -> &Arc { - self.0 - } - - pub(crate) fn get_selection_set_mut(&mut self) -> &mut SelectionSet { - &mut Arc::make_mut(self.0).selection_set - } - } - - pub(crate) enum Entry<'a> { - Occupied(OccupiedEntry<'a>), - Vacant(VacantEntry<'a>), - } - - impl<'a> Entry<'a> { - pub(crate) fn or_insert( - self, - produce: impl FnOnce() -> Result, - ) -> Result, FederationError> { - match self { - Self::Occupied(entry) => Ok(entry.into_mut()), - Self::Vacant(entry) => entry.insert(produce()?), - } - } - } - - pub(crate) struct OccupiedEntry<'a>(indexmap::map::OccupiedEntry<'a, SelectionKey, Selection>); - - impl<'a> OccupiedEntry<'a> { - pub(crate) fn get(&self) -> &Selection { - self.0.get() - } - - pub(crate) fn into_mut(self) -> SelectionValue<'a> { - SelectionValue::new(self.0.into_mut()) - } - } - - pub(crate) struct VacantEntry<'a>(indexmap::map::VacantEntry<'a, SelectionKey, Selection>); - - impl<'a> VacantEntry<'a> { - pub(crate) fn key(&self) -> &SelectionKey { - self.0.key() - } - - pub(crate) fn insert( - self, - value: Selection, - ) -> Result, FederationError> { - if *self.key() != value.key() { - return Err(Internal { - message: format!( - "Key mismatch when inserting selection {} into vacant entry ", - value - ), - } - .into()); - } - Ok(SelectionValue::new(self.0.insert(value))) - } - } - - impl IntoIterator for SelectionMap { - type Item = as IntoIterator>::Item; - type IntoIter = as IntoIterator>::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - as IntoIterator>::into_iter(self.0) - } - } - - impl<'a> IntoIterator for &'a SelectionMap { - type Item = <&'a IndexMap as IntoIterator>::Item; - type IntoIter = <&'a IndexMap as IntoIterator>::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter() - } - } -} +mod selection_map; pub(crate) use selection_map::FieldSelectionValue; pub(crate) use selection_map::FragmentSpreadSelectionValue; +pub(crate) use selection_map::HasSelectionKey; pub(crate) use selection_map::InlineFragmentSelectionValue; +pub(crate) use selection_map::SelectionKey; pub(crate) use selection_map::SelectionMap; pub(crate) use selection_map::SelectionValue; -/// A selection "key" (unrelated to the federation `@key` directive) is an identifier of a selection -/// (field, inline fragment, or fragment spread) that is used to determine whether two selections -/// can be merged. -/// -/// In order to merge two selections they need to -/// * reference the same field/inline fragment -/// * specify the same directives -/// * directives have to be applied in the same order -/// * directive arguments order does not matter (they get automatically sorted by their names). -/// * selection cannot specify @defer directive -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] -pub(crate) enum SelectionKey { - Field { - /// The field alias (if specified) or field name in the resulting selection set. - response_name: Name, - /// directives applied on the field - #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] - directives: DirectiveList, - }, - FragmentSpread { - /// The name of the fragment. - fragment_name: Name, - /// Directives applied on the fragment spread (does not contain @defer). - #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] - directives: DirectiveList, - }, - InlineFragment { - /// The optional type condition of the fragment. - type_condition: Option, - /// Directives applied on the fragment spread (does not contain @defer). - #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] - directives: DirectiveList, - }, - Defer { - /// Unique selection ID used to distinguish deferred fragment spreads that cannot be merged. - #[cfg_attr(not(feature = "snapshot_tracing"), serde(skip))] - deferred_id: SelectionId, - }, -} - -impl SelectionKey { - /// Returns true if the selection key is `__typename` *without directives*. - pub(crate) fn is_typename_field(&self) -> bool { - matches!(self, SelectionKey::Field { response_name, directives } if *response_name == TYPENAME_FIELD && directives.is_empty()) - } - - /// Create a selection key for a specific field name. - /// - /// This is available for tests only as selection keys should not normally be created outside of - /// `HasSelectionKey::key`. - #[cfg(test)] - pub(crate) fn field_name(name: &str) -> Self { - SelectionKey::Field { - response_name: Name::new(name).unwrap(), - directives: Default::default(), - } - } -} - -pub(crate) trait HasSelectionKey { - fn key(&self) -> SelectionKey; -} - /// 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_more::IsVariant, Serialize)] @@ -822,6 +403,17 @@ impl Selection { } } + /// Returns true if the selection key is `__typename` *without directives*. + pub(crate) fn is_typename_field(&self) -> bool { + if let Selection::Field(field) = self { + *field.field.response_name() == TYPENAME_FIELD && field.field.directives.is_empty() + } else { + false + } + } + + /// Returns the conditions for inclusion of this selection. + /// /// # Errors /// Returns an error if the selection contains a fragment spread, or if any of the /// @skip/@include directives are invalid (per GraphQL validation rules). @@ -937,7 +529,7 @@ impl From for Selection { } impl HasSelectionKey for Selection { - fn key(&self) -> SelectionKey { + fn key(&self) -> SelectionKey<'_> { match self { Selection::Field(field_selection) => field_selection.key(), Selection::FragmentSpread(fragment_spread_selection) => fragment_spread_selection.key(), @@ -990,7 +582,6 @@ impl Fragment { mod field_selection { use std::hash::Hash; use std::hash::Hasher; - use std::ops::Deref; use apollo_compiler::Name; use serde::Serialize; @@ -1024,7 +615,7 @@ mod field_selection { } impl HasSelectionKey for FieldSelection { - fn key(&self) -> SelectionKey { + fn key(&self) -> SelectionKey<'_> { self.field.key() } } @@ -1039,33 +630,46 @@ mod field_selection { selection_set, } } + } - pub(crate) fn with_updated_alias(&self, alias: Name) -> Field { - let mut data = self.field.data().clone(); - data.alias = Some(alias); - Field::new(data) + // SiblingTypename indicates how the sibling __typename field should be restored. + // PORT_NOTE: The JS version used the empty string to indicate unaliased sibling typenames. + // Here we use an enum to make the distinction explicit. + #[derive(Debug, Clone, Serialize)] + pub(crate) enum SiblingTypename { + Unaliased, + Aliased(Name), // the sibling __typename has been aliased + } + + impl SiblingTypename { + pub(crate) fn alias(&self) -> Option<&Name> { + match self { + SiblingTypename::Unaliased => None, + SiblingTypename::Aliased(alias) => Some(alias), + } } } /// The non-selection-set data of `FieldSelection`, used with operation paths and graph /// paths. - #[derive(Clone, Serialize)] + #[derive(Debug, Clone, Serialize)] pub(crate) struct Field { - data: FieldData, - key: SelectionKey, - } - - impl std::fmt::Debug for Field { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.data.fmt(f) - } + #[serde(skip)] + pub(crate) schema: ValidFederationSchema, + pub(crate) field_position: FieldDefinitionPosition, + pub(crate) alias: Option, + #[serde(serialize_with = "crate::display_helpers::serialize_as_debug_string")] + pub(crate) arguments: ArgumentList, + #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] + pub(crate) directives: DirectiveList, + pub(crate) sibling_typename: Option, } impl PartialEq for Field { fn eq(&self, other: &Self) -> bool { - self.data.field_position.field_name() == other.data.field_position.field_name() - && self.key == other.key - && self.data.arguments == other.data.arguments + self.field_position.field_name() == other.field_position.field_name() + && self.key() == other.key() + && self.arguments == other.arguments } } @@ -1073,25 +677,26 @@ mod field_selection { impl Hash for Field { fn hash(&self, state: &mut H) { - self.data.field_position.field_name().hash(state); - self.key.hash(state); - self.data.arguments.hash(state); - } - } - - impl Deref for Field { - type Target = FieldData; - - fn deref(&self) -> &Self::Target { - &self.data + self.field_position.field_name().hash(state); + self.key().hash(state); + self.arguments.hash(state); } } impl Field { - pub(crate) fn new(data: FieldData) -> Self { + /// Create a trivial field selection without any arguments or directives. + #[cfg(test)] + pub(crate) fn from_position( + schema: &ValidFederationSchema, + field_position: FieldDefinitionPosition, + ) -> Self { Self { - key: data.key(), - data, + schema: schema.clone(), + field_position, + alias: None, + arguments: Default::default(), + directives: Default::default(), + sibling_typename: None, } } @@ -1102,14 +707,73 @@ mod field_selection { parent_type: &CompositeTypeDefinitionPosition, alias: Option, ) -> Self { - Self::new(FieldData { + Self { schema: schema.clone(), field_position: parent_type.introspection_typename_field(), alias, arguments: Default::default(), directives: Default::default(), sibling_typename: None, - }) + } + } + + pub(crate) fn schema(&self) -> &ValidFederationSchema { + &self.schema + } + + pub(crate) fn name(&self) -> &Name { + self.field_position.field_name() + } + + pub(crate) fn response_name(&self) -> &Name { + self.alias.as_ref().unwrap_or_else(|| self.name()) + } + + // Is this a plain simple __typename without any directive or alias? + pub(crate) fn is_plain_typename_field(&self) -> bool { + *self.field_position.field_name() == TYPENAME_FIELD + && self.directives.is_empty() + && self.alias.is_none() + } + + pub(crate) fn sibling_typename(&self) -> Option<&SiblingTypename> { + self.sibling_typename.as_ref() + } + + pub(crate) fn sibling_typename_mut(&mut self) -> &mut Option { + &mut self.sibling_typename + } + + pub(crate) fn as_path_element(&self) -> FetchDataPathElement { + FetchDataPathElement::Key(self.response_name().clone(), Default::default()) + } + + pub(crate) fn output_base_type(&self) -> Result { + let definition = self.field_position.get(self.schema.schema())?; + self.schema + .get_type(definition.ty.inner_named_type().clone()) + } + + pub(crate) fn is_leaf(&self) -> Result { + let base_type_position = self.output_base_type()?; + Ok(matches!( + base_type_position, + TypeDefinitionPosition::Scalar(_) | TypeDefinitionPosition::Enum(_) + )) + } + + pub(crate) fn with_updated_directives(&self, directives: impl Into) -> Self { + Self { + directives: directives.into(), + ..self.clone() + } + } + + pub(crate) fn with_updated_alias(&self, alias: Name) -> Self { + Self { + alias: Some(alias), + ..self.clone() + } } /// Turn this `Field` into a `FieldSelection` with the given sub-selection. If this is @@ -1120,7 +784,7 @@ mod field_selection { ) -> FieldSelection { if cfg!(debug_assertions) { if let Some(ref selection_set) = selection_set { - if let Ok(field_type) = self.data.output_base_type() { + if let Ok(field_type) = self.output_base_type() { if let Ok(field_type_position) = CompositeTypeDefinitionPosition::try_from(field_type) { @@ -1153,139 +817,23 @@ mod field_selection { selection_set, } } - - pub(crate) fn schema(&self) -> &ValidFederationSchema { - &self.data.schema - } - - pub(crate) fn data(&self) -> &FieldData { - &self.data - } - - // Is this a plain simple __typename without any directive or alias? - pub(crate) fn is_plain_typename_field(&self) -> bool { - *self.data.field_position.field_name() == TYPENAME_FIELD - && self.data.directives.is_empty() - && self.data.alias.is_none() - } - - pub(crate) fn sibling_typename(&self) -> Option<&SiblingTypename> { - self.data.sibling_typename.as_ref() - } - - pub(crate) fn sibling_typename_mut(&mut self) -> &mut Option { - &mut self.data.sibling_typename - } - - pub(crate) fn with_updated_directives( - &self, - directives: impl Into, - ) -> Field { - let mut data = self.data.clone(); - data.directives = directives.into(); - Self::new(data) - } - - pub(crate) fn as_path_element(&self) -> FetchDataPathElement { - FetchDataPathElement::Key(self.response_name(), Default::default()) - } } impl HasSelectionKey for Field { - fn key(&self) -> SelectionKey { - self.key.clone() - } - } - - // SiblingTypename indicates how the sibling __typename field should be restored. - // PORT_NOTE: The JS version used the empty string to indicate unaliased sibling typenames. - // Here we use an enum to make the distinction explicit. - #[derive(Debug, Clone, Serialize)] - pub(crate) enum SiblingTypename { - Unaliased, - Aliased(Name), // the sibling __typename has been aliased - } - - impl SiblingTypename { - pub(crate) fn alias(&self) -> Option<&Name> { - match self { - SiblingTypename::Unaliased => None, - SiblingTypename::Aliased(alias) => Some(alias), - } - } - } - - #[derive(Debug, Clone, Serialize)] - pub(crate) struct FieldData { - #[serde(skip)] - pub(crate) schema: ValidFederationSchema, - pub(crate) field_position: FieldDefinitionPosition, - pub(crate) alias: Option, - #[serde(serialize_with = "crate::display_helpers::serialize_as_debug_string")] - pub(crate) arguments: ArgumentList, - #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] - pub(crate) directives: DirectiveList, - pub(crate) sibling_typename: Option, - } - - impl FieldData { - #[cfg(test)] - /// Create a trivial field selection without any arguments or directives. - pub(crate) fn from_position( - schema: &ValidFederationSchema, - field_position: FieldDefinitionPosition, - ) -> Self { - Self { - schema: schema.clone(), - field_position, - alias: None, - arguments: Default::default(), - directives: Default::default(), - sibling_typename: None, - } - } - - pub(crate) fn name(&self) -> &Name { - self.field_position.field_name() - } - - pub(crate) fn response_name(&self) -> Name { - self.alias.clone().unwrap_or_else(|| self.name().clone()) - } - - pub(crate) fn output_base_type(&self) -> Result { - let definition = self.field_position.get(self.schema.schema())?; - self.schema - .get_type(definition.ty.inner_named_type().clone()) - } - - pub(crate) fn is_leaf(&self) -> Result { - let base_type_position = self.output_base_type()?; - Ok(matches!( - base_type_position, - TypeDefinitionPosition::Scalar(_) | TypeDefinitionPosition::Enum(_) - )) - } - } - - impl HasSelectionKey for FieldData { - fn key(&self) -> SelectionKey { + fn key(&self) -> SelectionKey<'_> { SelectionKey::Field { response_name: self.response_name(), - directives: self.directives.clone(), + directives: &self.directives, } } } } pub(crate) use field_selection::Field; -pub(crate) use field_selection::FieldData; pub(crate) use field_selection::FieldSelection; pub(crate) use field_selection::SiblingTypename; mod fragment_spread_selection { - use std::ops::Deref; - use apollo_compiler::Name; use serde::Serialize; @@ -1305,7 +853,7 @@ mod fragment_spread_selection { } impl HasSelectionKey for FragmentSpreadSelection { - fn key(&self) -> SelectionKey { + fn key(&self) -> SelectionKey<'_> { self.spread.key() } } @@ -1313,55 +861,8 @@ mod fragment_spread_selection { /// An analogue of the apollo-compiler type `FragmentSpread` with these changes: /// - Stores the schema (may be useful for directives). /// - Encloses collection types in `Arc`s to facilitate cheaper cloning. - #[derive(Clone, Serialize)] - pub(crate) struct FragmentSpread { - data: FragmentSpreadData, - key: SelectionKey, - } - - impl std::fmt::Debug for FragmentSpread { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.data.fmt(f) - } - } - - impl PartialEq for FragmentSpread { - fn eq(&self, other: &Self) -> bool { - self.key == other.key - } - } - - impl Eq for FragmentSpread {} - - impl Deref for FragmentSpread { - type Target = FragmentSpreadData; - - fn deref(&self) -> &Self::Target { - &self.data - } - } - - impl FragmentSpread { - pub(crate) fn new(data: FragmentSpreadData) -> Self { - Self { - key: data.key(), - data, - } - } - - pub(crate) fn data(&self) -> &FragmentSpreadData { - &self.data - } - } - - impl HasSelectionKey for FragmentSpread { - fn key(&self) -> SelectionKey { - self.key.clone() - } - } - #[derive(Debug, Clone, Serialize)] - pub(crate) struct FragmentSpreadData { + pub(crate) struct FragmentSpread { #[serde(skip)] pub(crate) schema: ValidFederationSchema, pub(crate) fragment_name: Name, @@ -1380,16 +881,24 @@ mod fragment_spread_selection { pub(crate) selection_id: SelectionId, } - impl HasSelectionKey for FragmentSpreadData { - fn key(&self) -> SelectionKey { + impl PartialEq for FragmentSpread { + fn eq(&self, other: &Self) -> bool { + self.key() == other.key() + } + } + + impl Eq for FragmentSpread {} + + impl HasSelectionKey for FragmentSpread { + fn key(&self) -> SelectionKey<'_> { if is_deferred_selection(&self.directives) { SelectionKey::Defer { - deferred_id: self.selection_id.clone(), + deferred_id: self.selection_id, } } else { SelectionKey::FragmentSpread { - fragment_name: self.fragment_name.clone(), - directives: self.directives.clone(), + fragment_name: &self.fragment_name, + directives: &self.directives, } } } @@ -1397,7 +906,6 @@ mod fragment_spread_selection { } pub(crate) use fragment_spread_selection::FragmentSpread; -pub(crate) use fragment_spread_selection::FragmentSpreadData; pub(crate) use fragment_spread_selection::FragmentSpreadSelection; impl FragmentSpreadSelection { @@ -1410,9 +918,9 @@ impl FragmentSpreadSelection { fragment_spread: &executable::FragmentSpread, fragment: &Node, ) -> Result { - let spread_data = FragmentSpreadData::from_fragment(fragment, &fragment_spread.directives); + let spread = FragmentSpread::from_fragment(fragment, &fragment_spread.directives); Ok(FragmentSpreadSelection { - spread: FragmentSpread::new(spread_data), + spread, selection_set: fragment.selection_set.clone(), }) } @@ -1421,9 +929,9 @@ impl FragmentSpreadSelection { fragment: &Node, directives: &executable::DirectiveList, ) -> Self { - let spread_data = FragmentSpreadData::from_fragment(fragment, directives); + let spread = FragmentSpread::from_fragment(fragment, directives); Self { - spread: FragmentSpread::new(spread_data), + spread, selection_set: fragment.selection_set.clone(), } } @@ -1450,13 +958,13 @@ impl FragmentSpreadSelection { parent_type_position: CompositeTypeDefinitionPosition, predicate: &mut impl FnMut(OpPathElement) -> Result, ) -> Result { - let inline_fragment = InlineFragment::new(InlineFragmentData { + let inline_fragment = InlineFragment { schema: self.spread.schema.clone(), parent_type_position, type_condition_position: Some(self.spread.type_condition_position.clone()), directives: self.spread.directives.clone(), - selection_id: self.spread.selection_id.clone(), - }); + selection_id: self.spread.selection_id, + }; if predicate(inline_fragment.into())? { return Ok(true); } @@ -1464,12 +972,12 @@ impl FragmentSpreadSelection { } } -impl FragmentSpreadData { +impl FragmentSpread { pub(crate) fn from_fragment( fragment: &Node, spread_directives: &executable::DirectiveList, - ) -> FragmentSpreadData { - FragmentSpreadData { + ) -> FragmentSpread { + FragmentSpread { schema: fragment.schema.clone(), fragment_name: fragment.name.clone(), type_condition_position: fragment.type_condition_position.clone(), @@ -1483,7 +991,6 @@ impl FragmentSpreadData { mod inline_fragment_selection { use std::hash::Hash; use std::hash::Hasher; - use std::ops::Deref; use serde::Serialize; @@ -1542,28 +1049,28 @@ mod inline_fragment_selection { } impl HasSelectionKey for InlineFragmentSelection { - fn key(&self) -> SelectionKey { + fn key(&self) -> SelectionKey<'_> { self.inline_fragment.key() } } /// The non-selection-set data of `InlineFragmentSelection`, used with operation paths and /// graph paths. - #[derive(Clone, Serialize)] + #[derive(Debug, Clone, Serialize)] pub(crate) struct InlineFragment { - data: InlineFragmentData, - key: SelectionKey, - } - - impl std::fmt::Debug for InlineFragment { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.data.fmt(f) - } + #[serde(skip)] + pub(crate) schema: ValidFederationSchema, + pub(crate) parent_type_position: CompositeTypeDefinitionPosition, + pub(crate) type_condition_position: Option, + #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] + pub(crate) directives: DirectiveList, + #[cfg_attr(not(feature = "snapshot_tracing"), serde(skip))] + pub(crate) selection_id: SelectionId, } impl PartialEq for InlineFragment { fn eq(&self, other: &Self) -> bool { - self.key == other.key + self.key() == other.key() } } @@ -1571,49 +1078,30 @@ mod inline_fragment_selection { impl Hash for InlineFragment { fn hash(&self, state: &mut H) { - self.key.hash(state); - } - } - - impl Deref for InlineFragment { - type Target = InlineFragmentData; - - fn deref(&self) -> &Self::Target { - &self.data + self.key().hash(state); } } impl InlineFragment { - pub(crate) fn new(data: InlineFragmentData) -> Self { - Self { - key: data.key(), - data, - } - } - pub(crate) fn schema(&self) -> &ValidFederationSchema { - &self.data.schema - } - - pub(crate) fn data(&self) -> &InlineFragmentData { - &self.data + &self.schema } pub(crate) fn with_updated_type_condition( &self, - new: Option, + type_condition_position: Option, ) -> Self { - let mut data = self.data().clone(); - data.type_condition_position = new; - Self::new(data) + Self { + type_condition_position, + ..self.clone() + } } - pub(crate) fn with_updated_directives( - &self, - directives: impl Into, - ) -> InlineFragment { - let mut data = self.data().clone(); - data.directives = directives.into(); - Self::new(data) + + pub(crate) fn with_updated_directives(&self, directives: impl Into) -> Self { + Self { + directives: directives.into(), + ..self.clone() + } } pub(crate) fn as_path_element(&self) -> Option { @@ -1623,27 +1111,7 @@ mod inline_fragment_selection { condition.type_name().clone(), )) } - } - - impl HasSelectionKey for InlineFragment { - fn key(&self) -> SelectionKey { - self.key.clone() - } - } - #[derive(Debug, Clone, Serialize)] - pub(crate) struct InlineFragmentData { - #[serde(skip)] - pub(crate) schema: ValidFederationSchema, - pub(crate) parent_type_position: CompositeTypeDefinitionPosition, - pub(crate) type_condition_position: Option, - #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] - pub(crate) directives: DirectiveList, - #[cfg_attr(not(feature = "snapshot_tracing"), serde(skip))] - pub(crate) selection_id: SelectionId, - } - - impl InlineFragmentData { pub(crate) fn defer_directive_arguments( &self, ) -> Result, FederationError> { @@ -1661,19 +1129,19 @@ mod inline_fragment_selection { } } - impl HasSelectionKey for InlineFragmentData { - fn key(&self) -> SelectionKey { + impl HasSelectionKey for InlineFragment { + fn key(&self) -> SelectionKey<'_> { if is_deferred_selection(&self.directives) { SelectionKey::Defer { - deferred_id: self.selection_id.clone(), + deferred_id: self.selection_id, } } else { SelectionKey::InlineFragment { type_condition: self .type_condition_position .as_ref() - .map(|pos| pos.type_name().clone()), - directives: self.directives.clone(), + .map(|pos| pos.type_name()), + directives: &self.directives, } } } @@ -1681,9 +1149,9 @@ mod inline_fragment_selection { } pub(crate) use inline_fragment_selection::InlineFragment; -pub(crate) use inline_fragment_selection::InlineFragmentData; pub(crate) use inline_fragment_selection::InlineFragmentSelection; +use self::selection_map::OwnedSelectionKey; use crate::schema::position::INTROSPECTION_TYPENAME_FIELD_NAME; /// A simple MultiMap implementation using IndexMap with Vec as its value type. @@ -1751,7 +1219,7 @@ impl SelectionSet { // one :( struct TopLevelFieldSplitter { parent_type: CompositeTypeDefinitionPosition, - starting_set: ::IntoIter, + starting_set: selection_map::IntoValues, stack: Vec<(OpPathElement, Self)>, } @@ -1759,7 +1227,7 @@ impl SelectionSet { fn new(selection_set: SelectionSet) -> Self { Self { parent_type: selection_set.type_position, - starting_set: Arc::unwrap_or_clone(selection_set.selections).into_iter(), + starting_set: Arc::unwrap_or_clone(selection_set.selections).into_values(), stack: Vec::new(), } } @@ -1772,7 +1240,7 @@ impl SelectionSet { loop { match self.stack.last_mut() { None => { - let selection = self.starting_set.next()?.1; + let selection = self.starting_set.next()?; if selection.is_field() { return Some(SelectionSet::from_selection( self.parent_type.clone(), @@ -1868,7 +1336,7 @@ impl SelectionSet { } pub(crate) fn contains_top_level_field(&self, field: &Field) -> Result { - if let Some(selection) = self.selections.get(&field.key()) { + if let Some(selection) = self.selections.get(field.key()) { let Selection::Field(field_selection) = selection else { return Err(SingleFederationError::Internal { message: format!( @@ -2020,7 +1488,7 @@ impl SelectionSet { destination: &mut Vec, selection_set: &SelectionSet, ) -> Result<(), FederationError> { - for (_, value) in selection_set.selections.iter() { + for value in selection_set.selections.values() { match value { Selection::Field(field_selection) => { let selections = match &field_selection.selection_set { @@ -2104,20 +1572,21 @@ impl SelectionSet { interface_types_with_interface_objects.contains(&InterfaceTypeDefinitionPosition { type_name: self.type_position.type_name().clone(), }); - let mut typename_field_key: Option = None; - let mut sibling_field_key: Option = None; + let mut typename_field_key: Option = None; + let mut sibling_field_key: Option = None; let mutable_selection_map = Arc::make_mut(&mut self.selections); - for (key, entry) in mutable_selection_map.iter_mut() { + for entry in mutable_selection_map.values_mut() { + let key = entry.key().to_owned_key(); match entry { SelectionValue::Field(mut field_selection) => { if field_selection.get().field.is_plain_typename_field() && !is_interface_object && typename_field_key.is_none() { - typename_field_key = Some(key.clone()); + typename_field_key = Some(key); } else if sibling_field_key.is_none() { - sibling_field_key = Some(key.clone()); + sibling_field_key = Some(key); } if let Some(field_selection_set) = field_selection.get_selection_set_mut() { @@ -2149,8 +1618,8 @@ impl SelectionSet { Some((_, Selection::Field(typename_field))), Some(SelectionValue::Field(mut sibling_field)), ) = ( - mutable_selection_map.remove(&typename_key), - mutable_selection_map.get_mut(&sibling_field_key), + mutable_selection_map.remove(typename_key.as_borrowed_key()), + mutable_selection_map.get_mut(sibling_field_key.as_borrowed_key()), ) { // Note that as we tag the element, we also record the alias used if any since that // needs to be preserved. @@ -2199,6 +1668,13 @@ impl SelectionSet { } } + /// Returns the conditions for inclusion of this selection set. + /// + /// This tries to be smart about including or excluding the whole selection set. + /// - If all selections have the same condition, returns that condition. + /// - If selections in the set have different conditions, the selection set must always be + /// included, so the individual selections can be evaluated. + /// /// # Errors /// Returns an error if the selection set contains a fragment spread, or if any of the /// @skip/@include directives are invalid (per GraphQL validation rules). @@ -2272,8 +1748,8 @@ impl SelectionSet { sub_selection_updates.extend( sub_selection_set .selections - .iter() - .map(|(k, v)| (k.clone(), v.clone())), + .values() + .map(|v| (v.key(), v.clone())), ); } } @@ -2343,19 +1819,21 @@ impl SelectionSet { let mut updated_selections = MultiIndexMap::new(); updated_selections.extend( self.selections - .iter() + .values() .take(index) - .map(|(k, v)| (k.clone(), v.clone())), + .map(|v| (v.key().to_owned_key(), v.clone())), ); let mut update_new_selection = |selection| match selection { SelectionMapperReturn::None => {} // Removed; Skip it. SelectionMapperReturn::Selection(new_selection) => { - updated_selections.insert(new_selection.key(), new_selection) - } - SelectionMapperReturn::SelectionList(new_selections) => { - updated_selections.extend(new_selections.into_iter().map(|s| (s.key(), s))) + updated_selections.insert(new_selection.key().to_owned_key(), new_selection) } + SelectionMapperReturn::SelectionList(new_selections) => updated_selections.extend( + new_selections + .into_iter() + .map(|s| (s.key().to_owned_key(), s)), + ), }; // Now update the rest of the selections using the `mapper` function. @@ -2458,11 +1936,11 @@ impl SelectionSet { fn has_top_level_typename_field(&self) -> bool { const TYPENAME_KEY: SelectionKey = SelectionKey::Field { - response_name: TYPENAME_FIELD, - directives: DirectiveList::new(), + response_name: &TYPENAME_FIELD, + directives: &DirectiveList::new(), }; - self.selections.contains_key(&TYPENAME_KEY) + self.selections.contains_key(TYPENAME_KEY) } /// Adds a path, and optional some selections following that path, to this selection map. @@ -2671,7 +2149,7 @@ impl SelectionSet { selection_map.insert(selection.clone()); } else { let updated_field = match alias { - Some(alias) => field.with_updated_alias(alias.alias.clone()), + Some(alias) => field.field.with_updated_alias(alias.alias.clone()), None => field.field.clone(), }; selection_map @@ -2713,7 +2191,7 @@ impl SelectionSet { fn fields_in_set(&self) -> Vec { let mut fields = Vec::new(); - for (_key, selection) in self.selections.iter() { + for selection in self.selections.values() { match selection { Selection::Field(field) => fields.push(CollectedFieldInSet { path: Vec::new(), @@ -2831,29 +2309,29 @@ impl SelectionSet { } impl IntoIterator for SelectionSet { - type Item = as IntoIterator>::Item; - type IntoIter = as IntoIterator>::IntoIter; + type Item = Selection; + type IntoIter = selection_map::IntoValues; fn into_iter(self) -> Self::IntoIter { - Arc::unwrap_or_clone(self.selections).into_iter() + Arc::unwrap_or_clone(self.selections).into_values() } } impl<'a> IntoIterator for &'a SelectionSet { - type Item = <&'a IndexMap as IntoIterator>::Item; - type IntoIter = <&'a IndexMap as IntoIterator>::IntoIter; + type Item = &'a Selection; + type IntoIter = selection_map::Values<'a>; fn into_iter(self) -> Self::IntoIter { - self.selections.as_ref().into_iter() + self.selections.values() } } pub(crate) struct FieldSelectionsIter<'sel> { - stack: Vec>, + stack: Vec>, } impl<'sel> FieldSelectionsIter<'sel> { - fn new(iter: indexmap::map::Values<'sel, SelectionKey, Selection>) -> Self { + fn new(iter: selection_map::Values<'sel>) -> Self { Self { stack: vec![iter] } } } @@ -2936,7 +2414,7 @@ fn compute_aliases_for_non_merging_fields( let response_name = field.field.response_name(); let field_type = &field.field.field_position.get(field_schema)?.ty; - match seen_response_names.get(&response_name) { + match seen_response_names.get(response_name) { Some(previous) => { if &previous.field_name == field_name && types_can_be_merged(&previous.field_type, field_type, schema.schema())? @@ -2967,7 +2445,7 @@ fn compute_aliases_for_non_merging_fields( selections: field.selection_set.clone(), }); seen_response_names.insert( - response_name, + response_name.clone(), SeenResponseName { field_name: previous.field_name.clone(), field_type: previous.field_type.clone(), @@ -2979,7 +2457,7 @@ fn compute_aliases_for_non_merging_fields( } } else { // We need to alias the new occurence. - let alias = gen_alias_name(&response_name, &seen_response_names); + let alias = gen_alias_name(response_name, &seen_response_names); // Given how we generate aliases, it's is very unlikely that the generated alias will conflict with any of the other response name // at the level, but it's theoretically possible. By adding the alias to the seen names, we ensure that in the remote change that @@ -3009,7 +2487,7 @@ fn compute_aliases_for_non_merging_fields( alias_collector.push(FieldToAlias { path, - response_name, + response_name: response_name.clone(), alias, }) } @@ -3030,7 +2508,7 @@ fn compute_aliases_for_non_merging_fields( None => None, }; seen_response_names.insert( - response_name, + response_name.clone(), SeenResponseName { field_name: field_name.clone(), field_type: field_type.clone(), @@ -3061,7 +2539,7 @@ fn gen_alias_name(base_name: &Name, unavailable_names: &IndexMap Self { + fn with_updated_element(&self, field: Field) -> Self { Self { - field: Field::new(element), - ..self.clone() + field, + selection_set: self.selection_set.clone(), } } @@ -3203,13 +2681,13 @@ impl InlineFragmentSelection { }; let new_selection_set = SelectionSet::from_selection_set(&inline_fragment.selection_set, fragments, schema)?; - let new_inline_fragment = InlineFragment::new(InlineFragmentData { + let new_inline_fragment = InlineFragment { schema: schema.clone(), parent_type_position: parent_type_position.clone(), type_condition_position, directives: inline_fragment.directives.clone().into(), selection_id: SelectionId::new(), - }); + }; Ok(InlineFragmentSelection::new( new_inline_fragment, new_selection_set, @@ -3242,7 +2720,7 @@ impl InlineFragmentSelection { // Note: We assume that fragment_spread_selection.spread.type_condition_position is the same as // fragment_spread_selection.selection_set.type_position. Ok(InlineFragmentSelection::new( - InlineFragment::new(InlineFragmentData { + InlineFragment { schema: fragment_spread_selection.spread.schema.clone(), parent_type_position, type_condition_position: Some( @@ -3253,7 +2731,7 @@ impl InlineFragmentSelection { ), directives: fragment_spread_selection.spread.directives.clone(), selection_id: SelectionId::new(), - }), + }, fragment_spread_selection .selection_set .expand_all_fragments()?, @@ -3267,14 +2745,14 @@ impl InlineFragmentSelection { selection_set: SelectionSet, directives: DirectiveList, ) -> Self { - let inline_fragment_data = InlineFragmentData { + let inline_fragment_data = InlineFragment { schema: selection_set.schema.clone(), parent_type_position, type_condition_position: selection_set.type_position.clone().into(), directives, selection_id: SelectionId::new(), }; - InlineFragmentSelection::new(InlineFragment::new(inline_fragment_data), selection_set) + InlineFragmentSelection::new(inline_fragment_data, selection_set) } pub(crate) fn casted_type(&self) -> &CompositeTypeDefinitionPosition { @@ -3474,8 +2952,7 @@ impl NamedFragments { } if selection_set.selections.len() == 1 { // true if NOT field selection OR non-leaf field - return if let Some((_, Selection::Field(field_selection))) = - selection_set.selections.first() + return if let Some(Selection::Field(field_selection)) = selection_set.selections.first() { field_selection.selection_set.is_some() } else { @@ -3504,7 +2981,7 @@ pub(crate) struct NormalizedDefer { } struct DeferNormalizer { - used_labels: HashSet, + used_labels: IndexSet, assigned_labels: IndexSet, conditions: IndexMap>, label_offset: usize, @@ -3513,31 +2990,22 @@ struct DeferNormalizer { impl DeferNormalizer { fn new(selection_set: &SelectionSet) -> Result { let mut digest = Self { - used_labels: HashSet::default(), + used_labels: IndexSet::default(), label_offset: 0, assigned_labels: IndexSet::default(), conditions: IndexMap::default(), }; - let mut stack = selection_set - .into_iter() - .map(|(_, sel)| sel) - .collect::>(); + let mut stack = selection_set.into_iter().collect::>(); while let Some(selection) = stack.pop() { if let Selection::InlineFragment(inline) = selection { - if let Some(args) = inline.inline_fragment.data().defer_directive_arguments()? { + if let Some(args) = inline.inline_fragment.defer_directive_arguments()? { let DeferDirectiveArguments { label, if_: _ } = args; if let Some(label) = label { digest.used_labels.insert(label); } } } - stack.extend( - selection - .selection_set() - .into_iter() - .flatten() - .map(|(_, sel)| sel), - ); + stack.extend(selection.selection_set().into_iter().flatten()); } Ok(digest) } @@ -3644,9 +3112,9 @@ impl FragmentSpread { } fn without_defer(&self, filter: DeferFilter<'_>) -> Result { - let mut data = self.data().clone(); - filter.remove_defer(&mut data.directives, data.schema.schema()); - Ok(Self::new(data)) + let mut without_defer = self.clone(); + filter.remove_defer(&mut without_defer.directives, without_defer.schema.schema()); + Ok(without_defer) } } @@ -3663,9 +3131,9 @@ impl InlineFragment { } fn without_defer(&self, filter: DeferFilter<'_>) -> Result { - let mut data = self.data().clone(); - filter.remove_defer(&mut data.directives, data.schema.schema()); - Ok(Self::new(data)) + let mut without_defer = self.clone(); + filter.remove_defer(&mut without_defer.directives, without_defer.schema.schema()); + Ok(without_defer) } } @@ -3844,8 +3312,8 @@ impl SelectionSet { selections, } = self; Arc::unwrap_or_clone(selections) - .into_iter() - .map(|(_, sel)| sel.normalize_defer(normalizer)) + .into_values() + .map(|sel| sel.normalize_defer(normalizer)) .try_collect() .map(|selections| Self { schema, diff --git a/apollo-federation/src/operation/optimize.rs b/apollo-federation/src/operation/optimize.rs index bde0b6dfa9..c7e54b23f7 100644 --- a/apollo-federation/src/operation/optimize.rs +++ b/apollo-federation/src/operation/optimize.rs @@ -51,6 +51,7 @@ use super::Field; use super::FieldSelection; use super::Fragment; use super::FragmentSpreadSelection; +use super::HasSelectionKey; use super::InlineFragmentSelection; use super::NamedFragments; use super::Operation; @@ -60,7 +61,6 @@ use super::SelectionOrSet; use super::SelectionSet; use crate::error::FederationError; use crate::operation::FragmentSpread; -use crate::operation::FragmentSpreadData; use crate::operation::SelectionValue; use crate::schema::position::CompositeTypeDefinitionPosition; @@ -228,10 +228,7 @@ impl Selection { let diff = self_sub_selection.minus(other_sub_selection)?; if !diff.is_empty() { return self - .with_updated_selections( - self_sub_selection.type_position.clone(), - diff.into_iter().map(|(_, v)| v), - ) + .with_updated_selections(self_sub_selection.type_position.clone(), diff) .map(Some); } } @@ -251,10 +248,7 @@ impl Selection { return Ok(None); } else { return self - .with_updated_selections( - self_sub_selection.type_position.clone(), - common.into_iter().map(|(_, v)| v), - ) + .with_updated_selections(self_sub_selection.type_position.clone(), common) .map(Some); } } @@ -268,9 +262,9 @@ impl SelectionSet { pub(crate) fn minus(&self, other: &SelectionSet) -> Result { let iter = self .selections - .iter() - .map(|(k, v)| { - if let Some(other_v) = other.selections.get(k) { + .values() + .map(|v| { + if let Some(other_v) = other.selections.get(v.key()) { v.minus(other_v) } else { Ok(Some(v.clone())) @@ -297,9 +291,9 @@ impl SelectionSet { let iter = self .selections - .iter() - .map(|(k, v)| { - if let Some(other_v) = other.selections.get(k) { + .values() + .map(|v| { + if let Some(other_v) = other.selections.get(v.key()) { v.intersection(other_v) } else { Ok(None) @@ -413,7 +407,7 @@ impl FieldsConflictValidator { for selection_set in level { for field_selection in selection_set.field_selections() { let response_name = field_selection.field.response_name(); - let at_response_name = at_level.entry(response_name).or_default(); + let at_response_name = at_level.entry(response_name.clone()).or_default(); let entry = at_response_name .entry(field_selection.field.clone()) .or_default(); @@ -443,7 +437,7 @@ impl FieldsConflictValidator { fn for_field<'v>(&'v self, field: &Field) -> impl Iterator> + 'v { self.by_response_name - .get(&field.response_name()) + .get(field.response_name()) .into_iter() .flat_map(|by_response_name| by_response_name.values()) .flatten() @@ -682,10 +676,11 @@ impl FragmentRestrictionAtType { // Using `F` in those cases is, while not 100% incorrect, at least not productive, and so we // skip it that case. This is essentially an optimization. fn is_useless(&self) -> bool { - match self.selections.selections.as_slice().split_first() { - None => true, - Some((first, rest)) => rest.is_empty() && first.0.is_typename_field(), - } + let mut iter = self.selections.iter(); + let Some(first) = iter.next() else { + return true; + }; + iter.next().is_none() && first.is_typename_field() } } @@ -751,7 +746,7 @@ impl Fragment { return false; } - self.selection_set.selections.iter().any(|(_, selection)| { + self.selection_set.selections.values().any(|selection| { matches!( selection, Selection::FragmentSpread(fragment) if fragment.spread.fragment_name == *other_fragment_name @@ -1384,7 +1379,7 @@ impl InlineFragmentSelection { // case they should be kept on the spread. // PORT_NOTE: We are assuming directives on fragment definitions are // carried over to their spread sites as JS version does, which - // is handled differently in Rust version (see `FragmentSpreadData`). + // is handled differently in Rust version (see `FragmentSpread`). let directives: executable::DirectiveList = self .inline_fragment .directives @@ -1662,7 +1657,7 @@ impl FragmentGenerator { selection_set.type_position.clone(), ); - for (_key, selection) in Arc::make_mut(&mut selection_set.selections).iter_mut() { + for selection in Arc::make_mut(&mut selection_set.selections).values_mut() { match selection { SelectionValue::Field(mut field) => { if let Some(selection_set) = field.get_selection_set_mut() { @@ -1751,14 +1746,14 @@ impl FragmentGenerator { }; new_selection_set.add_local_selection(&Selection::from( FragmentSpreadSelection { - spread: FragmentSpread::new(FragmentSpreadData { + spread: FragmentSpread { schema: selection_set.schema.clone(), fragment_name: existing.name.clone(), type_condition_position: existing.type_condition_position.clone(), directives: skip_include.into(), fragment_directives: existing.directives.clone(), selection_id: crate::operation::SelectionId::new(), - }), + }, selection_set: existing.selection_set.clone(), }, ))?; @@ -3507,14 +3502,14 @@ mod tests { match path.split_first() { None => { // Base case - Arc::make_mut(&mut ss.selections).clear(); + ss.selections = Default::default(); Ok(()) } Some((first, rest)) => { - let result = Arc::make_mut(&mut ss.selections).get_mut(&SelectionKey::Field { - response_name: (*first).clone(), - directives: Default::default(), + let result = Arc::make_mut(&mut ss.selections).get_mut(SelectionKey::Field { + response_name: first, + directives: &Default::default(), }); let Some(mut value) = result else { return Err(FederationError::internal("No matching field found")); diff --git a/apollo-federation/src/operation/rebase.rs b/apollo-federation/src/operation/rebase.rs index 5e2d29c75d..09b8799067 100644 --- a/apollo-federation/src/operation/rebase.rs +++ b/apollo-federation/src/operation/rebase.rs @@ -11,10 +11,8 @@ use super::Field; use super::FieldSelection; use super::Fragment; use super::FragmentSpread; -use super::FragmentSpreadData; use super::FragmentSpreadSelection; use super::InlineFragment; -use super::InlineFragmentData; use super::InlineFragmentSelection; use super::NamedFragments; use super::OperationElement; @@ -24,6 +22,7 @@ use super::SelectionSet; use super::TYPENAME_FIELD; use crate::ensure; use crate::error::FederationError; +use crate::link::federation_spec_definition::get_federation_spec_definition_from_subgraph; use crate::schema::position::CompositeTypeDefinitionPosition; use crate::schema::position::OutputTypeDefinitionPosition; use crate::schema::ValidFederationSchema; @@ -191,28 +190,28 @@ impl Field { } .into()) } else { - let mut updated_field_data = self.data().clone(); - updated_field_data.schema = schema.clone(); - updated_field_data.field_position = parent_type.introspection_typename_field(); - Ok(Field::new(updated_field_data)) + let mut updated_field = self.clone(); + updated_field.schema = schema.clone(); + updated_field.field_position = parent_type.introspection_typename_field(); + Ok(updated_field) }; } let field_from_parent = parent_type.field(self.name().clone())?; - return if field_from_parent.try_get(schema.schema()).is_some() + if field_from_parent.try_get(schema.schema()).is_some() && self.can_rebase_on(parent_type)? { - let mut updated_field_data = self.data().clone(); - updated_field_data.schema = schema.clone(); - updated_field_data.field_position = field_from_parent; - Ok(Field::new(updated_field_data)) + let mut updated_field = self.clone(); + updated_field.schema = schema.clone(); + updated_field.field_position = field_from_parent; + Ok(updated_field) } else { Err(RebaseError::CannotRebase { field_position: self.field_position.clone(), parent_type: parent_type.clone(), } .into()) - }; + } } /// Verifies whether given field can be rebase on following parent type. @@ -267,19 +266,42 @@ impl Field { }; return Ok(Some(schema.get_type(type_name.clone())?.try_into()?)); } - if self.can_rebase_on(parent_type)? { - let Some(type_name) = parent_type - .field(data.field_position.field_name().clone()) - .ok() - .and_then(|field_pos| field_pos.get(schema.schema()).ok()) - .map(|field| field.ty.inner_named_type()) - else { + if !self.can_rebase_on(parent_type)? { + return Ok(None); + } + let Some(field_definition) = parent_type + .field(data.field_position.field_name().clone()) + .ok() + .and_then(|field_pos| field_pos.get(schema.schema()).ok()) + else { + return Ok(None); + }; + if let Ok(federation_spec_definition) = get_federation_spec_definition_from_subgraph(schema) + { + let from_context_directive_definition_name = &federation_spec_definition + .from_context_directive_definition(schema)? + .name; + // We need to prevent arguments with `@fromContext` from being lost/overwriten. If the + // would-be parent type's field has `@fromContext` and one (or more) of this field's + // arguments doesn't exist in the would-be parent's field, rebasing would loose that + // context. + if field_definition.arguments.iter().any(|arg_definition| { + arg_definition + .directives + .has(from_context_directive_definition_name) + && !data + .arguments + .iter() + .any(|arg| arg.name == arg_definition.name) + }) { return Ok(None); - }; - Ok(Some(schema.get_type(type_name.clone())?.try_into()?)) - } else { - Ok(None) + } } + Ok(Some( + schema + .get_type(field_definition.ty.inner_named_type().clone())? + .try_into()?, + )) } } @@ -390,10 +412,10 @@ impl FragmentSpread { &named_fragment.type_condition_position, &self.schema, ) { - Ok(FragmentSpread::new(FragmentSpreadData::from_fragment( + Ok(FragmentSpread::from_fragment( named_fragment, &self.directives, - ))) + )) } else { Err(RebaseError::NonIntersectingCondition { type_condition: named_fragment.type_condition_position.clone().into(), @@ -474,23 +496,20 @@ impl FragmentSpreadSelection { Err(RebaseError::EmptySelectionSet.into()) } else { Ok(InlineFragmentSelection::new( - InlineFragment::new(InlineFragmentData { + InlineFragment { schema: schema.clone(), parent_type_position: parent_type.clone(), type_condition_position: None, directives: Default::default(), selection_id: SelectionId::new(), - }), + }, expanded_selection_set, ) .into()) }; } - let spread = FragmentSpread::new(FragmentSpreadData::from_fragment( - named_fragment, - &self.spread.directives, - )); + let spread = FragmentSpread::from_fragment(named_fragment, &self.spread.directives); Ok(FragmentSpreadSelection { spread, selection_set: named_fragment.selection_set.clone(), @@ -508,7 +527,7 @@ impl FragmentSpreadSelection { } } -impl InlineFragmentData { +impl InlineFragment { fn casted_type_if_add_to( &self, parent_type: &CompositeTypeDefinitionPosition, @@ -517,33 +536,18 @@ impl InlineFragmentData { if self.schema == *schema && self.parent_type_position == *parent_type { return Some(self.casted_type()); } - match self.can_rebase_on(parent_type, schema) { - (false, _) => None, - (true, None) => Some(parent_type.clone()), - (true, Some(ty)) => Some(ty), - } - } - - fn can_rebase_on( - &self, - parent_type: &CompositeTypeDefinitionPosition, - schema: &ValidFederationSchema, - ) -> (bool, Option) { let Some(ty) = self.type_condition_position.as_ref() else { - return (true, None); + return Some(parent_type.clone()); }; - match schema + + let rebased_type = schema .get_type(ty.type_name().clone()) .ok() - .and_then(|ty| CompositeTypeDefinitionPosition::try_from(ty).ok()) - { - Some(ty) if runtime_types_intersect(parent_type, &ty, schema) => (true, Some(ty)), - _ => (false, None), - } + .and_then(|ty| CompositeTypeDefinitionPosition::try_from(ty).ok())?; + + runtime_types_intersect(parent_type, &rebased_type, schema).then_some(rebased_type) } -} -impl InlineFragment { pub(crate) fn rebase_on( &self, parent_type: &CompositeTypeDefinitionPosition, @@ -566,11 +570,11 @@ impl InlineFragment { } .into()) } else { - let mut rebased_fragment_data = self.data().clone(); - rebased_fragment_data.parent_type_position = parent_type.clone(); - rebased_fragment_data.type_condition_position = rebased_condition; - rebased_fragment_data.schema = schema.clone(); - Ok(InlineFragment::new(rebased_fragment_data)) + let mut rebased_fragment = self.clone(); + rebased_fragment.parent_type_position = parent_type.clone(); + rebased_fragment.type_condition_position = rebased_condition; + rebased_fragment.schema = schema.clone(); + Ok(rebased_fragment) } } @@ -710,8 +714,8 @@ impl SelectionSet { ) -> Result { let rebased_results = self .selections - .iter() - .map(|(_, selection)| { + .values() + .map(|selection| { selection.rebase_inner( parent_type, named_fragments, diff --git a/apollo-federation/src/operation/selection_map.rs b/apollo-federation/src/operation/selection_map.rs new file mode 100644 index 0000000000..19857ba274 --- /dev/null +++ b/apollo-federation/src/operation/selection_map.rs @@ -0,0 +1,647 @@ +use std::borrow::Cow; +use std::hash::BuildHasher; +use std::sync::Arc; + +use apollo_compiler::Name; +use hashbrown::DefaultHashBuilder; +use hashbrown::HashTable; +use serde::ser::SerializeSeq; +use serde::Serialize; + +use crate::error::FederationError; +use crate::operation::field_selection::FieldSelection; +use crate::operation::fragment_spread_selection::FragmentSpreadSelection; +use crate::operation::inline_fragment_selection::InlineFragmentSelection; +use crate::operation::DirectiveList; +use crate::operation::Selection; +use crate::operation::SelectionId; +use crate::operation::SelectionSet; +use crate::operation::SiblingTypename; + +/// A selection "key" (unrelated to the federation `@key` directive) is an identifier of a selection +/// (field, inline fragment, or fragment spread) that is used to determine whether two selections +/// can be merged. +/// +/// In order to merge two selections they need to +/// * reference the same field/inline fragment +/// * specify the same directives +/// * directives have to be applied in the same order +/// * directive arguments order does not matter (they get automatically sorted by their names). +/// * selection cannot specify @defer directive +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] +pub(crate) enum SelectionKey<'a> { + Field { + /// The field alias (if specified) or field name in the resulting selection set. + response_name: &'a Name, + /// directives applied on the field + #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] + directives: &'a DirectiveList, + }, + FragmentSpread { + /// The name of the fragment. + fragment_name: &'a Name, + /// Directives applied on the fragment spread (does not contain @defer). + #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] + directives: &'a DirectiveList, + }, + InlineFragment { + /// The optional type condition of the fragment. + type_condition: Option<&'a Name>, + /// Directives applied on the fragment spread (does not contain @defer). + #[serde(serialize_with = "crate::display_helpers::serialize_as_string")] + directives: &'a DirectiveList, + }, + Defer { + /// Unique selection ID used to distinguish deferred fragment spreads that cannot be merged. + #[cfg_attr(not(feature = "snapshot_tracing"), serde(skip))] + deferred_id: SelectionId, + }, +} + +impl SelectionKey<'_> { + /// Get an owned structure representing the selection key, for use in map keys + /// that are not a plain selection map. + pub(crate) fn to_owned_key(self) -> OwnedSelectionKey { + match self { + Self::Field { + response_name, + directives, + } => OwnedSelectionKey::Field { + response_name: response_name.clone(), + directives: directives.clone(), + }, + Self::FragmentSpread { + fragment_name, + directives, + } => OwnedSelectionKey::FragmentSpread { + fragment_name: fragment_name.clone(), + directives: directives.clone(), + }, + Self::InlineFragment { + type_condition, + directives, + } => OwnedSelectionKey::InlineFragment { + type_condition: type_condition.cloned(), + directives: directives.clone(), + }, + Self::Defer { deferred_id } => OwnedSelectionKey::Defer { deferred_id }, + } + } +} + +/// An owned structure representing the selection key, for use in map keys +/// that are not a plain selection map. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(crate) enum OwnedSelectionKey { + Field { + response_name: Name, + directives: DirectiveList, + }, + FragmentSpread { + fragment_name: Name, + directives: DirectiveList, + }, + InlineFragment { + type_condition: Option, + directives: DirectiveList, + }, + Defer { + deferred_id: SelectionId, + }, +} + +impl OwnedSelectionKey { + /// Get a plain, borrowed selection key, that can be used for indexing into a selection map. + pub(crate) fn as_borrowed_key(&self) -> SelectionKey<'_> { + match self { + OwnedSelectionKey::Field { + response_name, + directives, + } => SelectionKey::Field { + response_name, + directives, + }, + OwnedSelectionKey::FragmentSpread { + fragment_name, + directives, + } => SelectionKey::FragmentSpread { + fragment_name, + directives, + }, + OwnedSelectionKey::InlineFragment { + type_condition, + directives, + } => SelectionKey::InlineFragment { + type_condition: type_condition.as_ref(), + directives, + }, + OwnedSelectionKey::Defer { deferred_id } => SelectionKey::Defer { + deferred_id: *deferred_id, + }, + } + } +} + +impl<'a> SelectionKey<'a> { + /// Create a selection key for a specific field name. + /// + /// This is available for tests only as selection keys should not normally be created outside of + /// `HasSelectionKey::key`. + #[cfg(test)] + pub(crate) fn field_name(name: &'a Name) -> Self { + static EMPTY_LIST: DirectiveList = DirectiveList::new(); + SelectionKey::Field { + response_name: name, + directives: &EMPTY_LIST, + } + } +} + +pub(crate) trait HasSelectionKey { + fn key(&self) -> SelectionKey<'_>; +} + +#[derive(Clone)] +struct Bucket { + index: usize, + hash: u64, +} + +/// A selection map is the underlying representation of a selection set. It contains an ordered +/// list of selections with unique selection keys. Selections with the same key should be merged +/// together by the user of this structure: the selection map API itself will overwrite selections +/// with the same key. +/// +/// Once a selection is in the selection map, it must not be modified in a way that changes the +/// selection key. Therefore, the selection map only hands out mutable access through the +/// SelectionValue types, which expose the parts of selections that are safe to modify. +#[derive(Clone)] +pub(crate) struct SelectionMap { + hash_builder: DefaultHashBuilder, + table: HashTable, + selections: Vec, +} + +impl std::fmt::Debug for SelectionMap { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_set().entries(self.values()).finish() + } +} + +impl PartialEq for SelectionMap { + /// Compare two selection maps. This is order independent. + fn eq(&self, other: &Self) -> bool { + self.len() == other.len() + && self + .values() + .all(|left| other.get(left.key()).is_some_and(|right| left == right)) + } +} + +impl Eq for SelectionMap {} + +impl Serialize for SelectionMap { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut seq = serializer.serialize_seq(Some(self.len()))?; + for value in self.values() { + seq.serialize_element(value)?; + } + seq.end() + } +} + +impl Default for SelectionMap { + fn default() -> Self { + Self::new() + } +} + +pub(crate) type Values<'a> = std::slice::Iter<'a, Selection>; +pub(crate) type ValuesMut<'a> = + std::iter::Map, fn(&'a mut Selection) -> SelectionValue<'a>>; +pub(crate) type IntoValues = std::vec::IntoIter; + +/// Return an equality function taking an index into `selections` and returning if the index +/// matches the given key. +/// +/// The returned function panics if the index is out of bounds. +fn key_eq<'a>(selections: &'a [Selection], key: SelectionKey<'a>) -> impl Fn(&Bucket) -> bool + 'a { + move |bucket| selections[bucket.index].key() == key +} + +impl SelectionMap { + /// Create an empty selection map. + pub(crate) fn new() -> Self { + SelectionMap { + hash_builder: Default::default(), + table: HashTable::new(), + selections: Vec::new(), + } + } + + /// Returns the number of selections in the map. + pub(crate) fn len(&self) -> usize { + self.selections.len() + } + + /// Returns true if there are no selections in the map. + pub(crate) fn is_empty(&self) -> bool { + self.selections.is_empty() + } + + /// Returns the first selection in the map, or None if the map is empty. + pub(crate) fn first(&self) -> Option<&Selection> { + self.selections.first() + } + + /// Computes the hash of a selection key. + fn hash(&self, key: SelectionKey<'_>) -> u64 { + self.hash_builder.hash_one(key) + } + + /// Returns true if the given key exists in the map. + pub(crate) fn contains_key(&self, key: SelectionKey<'_>) -> bool { + let hash = self.hash(key); + self.table + .find(hash, key_eq(&self.selections, key)) + .is_some() + } + + /// Returns true if the given key exists in the map. + pub(crate) fn get(&self, key: SelectionKey<'_>) -> Option<&Selection> { + let hash = self.hash(key); + let bucket = self.table.find(hash, key_eq(&self.selections, key))?; + Some(&self.selections[bucket.index]) + } + + pub(crate) fn get_mut(&mut self, key: SelectionKey<'_>) -> Option> { + let hash = self.hash(key); + let bucket = self.table.find_mut(hash, key_eq(&self.selections, key))?; + Some(SelectionValue::new(&mut self.selections[bucket.index])) + } + + /// Insert a selection into the map. + fn raw_insert(&mut self, hash: u64, value: Selection) -> &mut Selection { + let index = self.selections.len(); + + self.table + .insert_unique(hash, Bucket { index, hash }, |existing| existing.hash); + + self.selections.push(value); + &mut self.selections[index] + } + + /// Resets and rebuilds the hash table. + /// + /// Preconditions: + /// - The table must have enough capacity for `self.selections.len()` elements. + fn rebuild_table_no_grow(&mut self) { + assert!(self.table.capacity() >= self.selections.len()); + self.table.clear(); + for (index, selection) in self.selections.iter().enumerate() { + let hash = self.hash(selection.key()); + self.table + .insert_unique(hash, Bucket { index, hash }, |existing| existing.hash); + } + } + + /// Decrements all the indices in the table starting at `pivot`. + fn decrement_table(&mut self, pivot: usize) { + for bucket in self.table.iter_mut() { + if bucket.index >= pivot { + bucket.index -= 1; + } + } + } + + pub(crate) fn insert(&mut self, value: Selection) { + let hash = self.hash(value.key()); + self.raw_insert(hash, value); + } + + /// Remove a selection from the map. Returns the selection and its numeric index. + pub(crate) fn remove(&mut self, key: SelectionKey<'_>) -> Option<(usize, Selection)> { + let hash = self.hash(key); + let entry = self + .table + .find_entry(hash, key_eq(&self.selections, key)) + .ok()?; + let (bucket, _) = entry.remove(); + let selection = self.selections.remove(bucket.index); + self.decrement_table(bucket.index); + Some((bucket.index, selection)) + } + + pub(crate) fn retain( + &mut self, + mut predicate: impl FnMut(SelectionKey<'_>, &Selection) -> bool, + ) { + self.selections.retain(|selection| { + let key = selection.key(); + predicate(key, selection) + }); + if self.selections.len() < self.table.len() { + // In theory, we could track which keys were removed, and adjust the indices based on + // that, but it's very tricky and it might not even be faster than just resetting the + // whole map. + self.rebuild_table_no_grow(); + } + assert!(self.selections.len() == self.table.len()); + } + + /// Iterate over all selections. + pub(crate) fn values(&self) -> Values<'_> { + self.selections.iter() + } + + /// Iterate over all selections. + pub(crate) fn values_mut(&mut self) -> ValuesMut<'_> { + self.selections.iter_mut().map(SelectionValue::new) + } + + /// Iterate over all selections. + pub(crate) fn into_values(self) -> IntoValues { + self.selections.into_iter() + } + + /// Provides mutable access to a selection key. A new selection can be inserted or an existing + /// selection modified. + pub(super) fn entry<'a>(&'a mut self, key: SelectionKey<'a>) -> Entry<'a> { + let hash = self.hash(key); + let slot = self.table.find_entry(hash, key_eq(&self.selections, key)); + match slot { + Ok(occupied) => { + let index = occupied.get().index; + let selection = &mut self.selections[index]; + Entry::Occupied(OccupiedEntry(selection)) + } + // We're not using `hashbrown`'s VacantEntry API here, because we have some custom + // insertion logic, it's easier to use `SelectionMap::raw_insert` to implement + // `VacantEntry::or_insert`. + Err(_) => Entry::Vacant(VacantEntry { + map: self, + hash, + key, + }), + } + } + + /// Add selections from another selection map to this one. If there are key collisions, the + /// selections are *overwritten*. + pub(crate) fn extend(&mut self, other: SelectionMap) { + for selection in other.into_values() { + self.insert(selection); + } + } + + /// Add selections from another selection map to this one. If there are key collisions, the + /// selections are *overwritten*. + pub(crate) fn extend_ref(&mut self, other: &SelectionMap) { + for selection in other.values() { + self.insert(selection.clone()); + } + } + + /// Returns the selection set resulting from "recursively" filtering any selection + /// that does not match the provided predicate. + /// This method calls `predicate` on every selection of the selection set, + /// not just top-level ones, and apply a "depth-first" strategy: + /// when the predicate is called on a given selection it is guaranteed that + /// filtering has happened on all the selections of its sub-selection. + pub(crate) fn filter_recursive_depth_first( + &self, + predicate: &mut dyn FnMut(&Selection) -> Result, + ) -> Result, FederationError> { + fn recur_sub_selections<'sel>( + selection: &'sel Selection, + predicate: &mut dyn FnMut(&Selection) -> Result, + ) -> Result, FederationError> { + Ok(match selection { + Selection::Field(field) => { + if let Some(sub_selections) = &field.selection_set { + match sub_selections.filter_recursive_depth_first(predicate)? { + Cow::Borrowed(_) => Cow::Borrowed(selection), + Cow::Owned(new) => { + Cow::Owned(Selection::from_field(field.field.clone(), Some(new))) + } + } + } else { + Cow::Borrowed(selection) + } + } + Selection::InlineFragment(fragment) => match fragment + .selection_set + .filter_recursive_depth_first(predicate)? + { + Cow::Borrowed(_) => Cow::Borrowed(selection), + Cow::Owned(selection_set) => Cow::Owned(Selection::InlineFragment(Arc::new( + InlineFragmentSelection::new( + fragment.inline_fragment.clone(), + selection_set, + ), + ))), + }, + Selection::FragmentSpread(_) => { + return Err(FederationError::internal("unexpected fragment spread")) + } + }) + } + let mut iter = self.values(); + let mut enumerated = (&mut iter).enumerate(); + let mut new_map: Self; + loop { + let Some((index, selection)) = enumerated.next() else { + return Ok(Cow::Borrowed(self)); + }; + let filtered = recur_sub_selections(selection, predicate)?; + let keep = predicate(&filtered)?; + if keep && matches!(filtered, Cow::Borrowed(_)) { + // Nothing changed so far, continue without cloning + continue; + } + + // Clone the map so far + new_map = self.selections[..index].iter().cloned().collect(); + + if keep { + new_map.insert(filtered.into_owned()); + } + break; + } + for selection in iter { + let filtered = recur_sub_selections(selection, predicate)?; + if predicate(&filtered)? { + new_map.insert(filtered.into_owned()); + } + } + Ok(Cow::Owned(new_map)) + } +} + +impl FromIterator for SelectionMap +where + A: Into, +{ + /// Create a selection map from an iterator of selections. On key collisions, *only the later + /// selection is used*. + fn from_iter>(iter: T) -> Self { + let mut map = Self::new(); + for selection in iter { + map.insert(selection.into()); + } + map + } +} + +/// A mutable reference to a `Selection` value in a `SelectionMap`, which +/// also disallows changing key-related data (to maintain the invariant that a value's key is +/// the same as it's map entry's key). +#[derive(Debug)] +pub(crate) enum SelectionValue<'a> { + Field(FieldSelectionValue<'a>), + FragmentSpread(FragmentSpreadSelectionValue<'a>), + InlineFragment(InlineFragmentSelectionValue<'a>), +} + +impl<'a> SelectionValue<'a> { + fn new(selection: &'a mut Selection) -> Self { + match selection { + Selection::Field(field_selection) => { + SelectionValue::Field(FieldSelectionValue::new(field_selection)) + } + Selection::FragmentSpread(fragment_spread_selection) => SelectionValue::FragmentSpread( + FragmentSpreadSelectionValue::new(fragment_spread_selection), + ), + Selection::InlineFragment(inline_fragment_selection) => SelectionValue::InlineFragment( + InlineFragmentSelectionValue::new(inline_fragment_selection), + ), + } + } + + pub(super) fn key(&self) -> SelectionKey<'_> { + match self { + Self::Field(field) => field.get().key(), + Self::FragmentSpread(frag) => frag.get().key(), + Self::InlineFragment(frag) => frag.get().key(), + } + } + + // This is used in operation::optimize tests + #[cfg(test)] + pub(super) fn get_selection_set_mut(&mut self) -> Option<&mut SelectionSet> { + match self { + SelectionValue::Field(field) => field.get_selection_set_mut(), + SelectionValue::FragmentSpread(frag) => Some(frag.get_selection_set_mut()), + SelectionValue::InlineFragment(frag) => Some(frag.get_selection_set_mut()), + } + } +} + +#[derive(Debug)] +pub(crate) struct FieldSelectionValue<'a>(&'a mut Arc); + +impl<'a> FieldSelectionValue<'a> { + pub(crate) fn new(field_selection: &'a mut Arc) -> Self { + Self(field_selection) + } + + pub(crate) fn get(&self) -> &Arc { + self.0 + } + + pub(crate) fn get_sibling_typename_mut(&mut self) -> &mut Option { + Arc::make_mut(self.0).field.sibling_typename_mut() + } + + pub(crate) fn get_selection_set_mut(&mut self) -> Option<&mut SelectionSet> { + Arc::make_mut(self.0).selection_set.as_mut() + } +} + +#[derive(Debug)] +pub(crate) struct FragmentSpreadSelectionValue<'a>(&'a mut Arc); + +impl<'a> FragmentSpreadSelectionValue<'a> { + pub(crate) fn new(fragment_spread_selection: &'a mut Arc) -> Self { + Self(fragment_spread_selection) + } + + pub(crate) fn get(&self) -> &Arc { + self.0 + } + + #[cfg(test)] + pub(crate) fn get_selection_set_mut(&mut self) -> &mut SelectionSet { + &mut Arc::make_mut(self.0).selection_set + } +} + +#[derive(Debug)] +pub(crate) struct InlineFragmentSelectionValue<'a>(&'a mut Arc); + +impl<'a> InlineFragmentSelectionValue<'a> { + pub(crate) fn new(inline_fragment_selection: &'a mut Arc) -> Self { + Self(inline_fragment_selection) + } + + pub(crate) fn get(&self) -> &Arc { + self.0 + } + + pub(crate) fn get_selection_set_mut(&mut self) -> &mut SelectionSet { + &mut Arc::make_mut(self.0).selection_set + } +} + +pub(crate) enum Entry<'a> { + Occupied(OccupiedEntry<'a>), + Vacant(VacantEntry<'a>), +} + +impl<'a> Entry<'a> { + pub(crate) fn or_insert( + self, + produce: impl FnOnce() -> Result, + ) -> Result, FederationError> { + match self { + Self::Occupied(entry) => Ok(entry.into_mut()), + Self::Vacant(entry) => entry.insert(produce()?), + } + } +} + +pub(crate) struct OccupiedEntry<'a>(&'a mut Selection); + +impl<'a> OccupiedEntry<'a> { + pub(crate) fn get(&self) -> &Selection { + self.0 + } + + pub(crate) fn into_mut(self) -> SelectionValue<'a> { + SelectionValue::new(self.0) + } +} + +pub(crate) struct VacantEntry<'a> { + map: &'a mut SelectionMap, + hash: u64, + key: SelectionKey<'a>, +} + +impl<'a> VacantEntry<'a> { + pub(crate) fn key(&self) -> SelectionKey<'a> { + self.key + } + + pub(crate) fn insert(self, value: Selection) -> Result, FederationError> { + if self.key() != value.key() { + return Err(FederationError::internal(format!( + "Key mismatch when inserting selection {value} into vacant entry " + ))); + }; + Ok(SelectionValue::new(self.map.raw_insert(self.hash, value))) + } +} diff --git a/apollo-federation/src/operation/simplify.rs b/apollo-federation/src/operation/simplify.rs index f7c339d210..29d4c55d9f 100644 --- a/apollo-federation/src/operation/simplify.rs +++ b/apollo-federation/src/operation/simplify.rs @@ -6,7 +6,6 @@ use apollo_compiler::name; use super::runtime_types_intersect; use super::DirectiveList; use super::Field; -use super::FieldData; use super::FieldSelection; use super::FragmentSpreadSelection; use super::InlineFragmentSelection; @@ -62,7 +61,7 @@ impl FieldSelection { let field_element = if self.field.schema() == schema && self.field.field_position == field_position { - self.field.data().clone() + self.field.clone() } else { self.field .with_updated_position(schema.clone(), field_position) @@ -89,7 +88,7 @@ impl FieldSelection { arguments: vec![(name!("if"), false).into()], }); let non_included_typename = Selection::from_field( - Field::new(FieldData { + Field { schema: schema.clone(), field_position: field_composite_type_position .introspection_typename_field(), @@ -97,7 +96,7 @@ impl FieldSelection { arguments: Default::default(), directives, sibling_typename: None, - }), + }, None, ); let mut typename_selection = SelectionMap::new(); @@ -229,14 +228,14 @@ impl InlineFragmentSelection { parent_type.introspection_typename_field() }; let typename_field_selection = Selection::from_field( - Field::new(FieldData { + Field { schema: schema.clone(), field_position: parent_typename_field, alias: None, arguments: Default::default(), directives, sibling_typename: None, - }), + }, None, ); @@ -260,7 +259,7 @@ impl InlineFragmentSelection { && this_condition.is_some_and(|c| c.is_abstract_type()) { let mut liftable_selections = SelectionMap::new(); - for (_, selection) in selection_set.selections.iter() { + for selection in selection_set.selections.values() { match selection { Selection::FragmentSpread(spread_selection) => { let type_condition = &spread_selection.spread.type_condition_position; @@ -296,7 +295,7 @@ impl InlineFragmentSelection { // Otherwise, if there are "liftable" selections, we must return a set comprised of those lifted selection, // and the current fragment _without_ those lifted selections. - if liftable_selections.len() > 0 { + if !liftable_selections.is_empty() { // Converting `... [on T] { }` into // `{ ... [on T] { } }`. // PORT_NOTE: It appears that this lifting could be repeatable (meaning lifted @@ -323,8 +322,8 @@ impl InlineFragmentSelection { // Since liftable_selections are changing their parent, we need to rebase them. liftable_selections = liftable_selections - .into_iter() - .map(|(_key, sel)| sel.rebase_on(parent_type, named_fragments, schema)) + .into_values() + .map(|sel| sel.rebase_on(parent_type, named_fragments, schema)) .collect::>()?; let mut final_selection_map = SelectionMap::new(); @@ -348,15 +347,29 @@ impl InlineFragmentSelection { } else { let rebased_inline_fragment = self.inline_fragment.rebase_on(parent_type, schema)?; let rebased_casted_type = rebased_inline_fragment.casted_type(); - let rebased_selection_set = - selection_set.rebase_on(&rebased_casted_type, named_fragments, schema)?; - Ok(Some( - Selection::InlineFragment(Arc::new(InlineFragmentSelection::new( - rebased_inline_fragment, - rebased_selection_set, - ))) - .into(), - )) + // Re-flatten with the rebased casted type, which could further flatten away. + let selection_set = selection_set.flatten_unnecessary_fragments( + &rebased_casted_type, + named_fragments, + schema, + )?; + if selection_set.is_empty() { + Ok(None) + } else { + // We need to rebase since the parent type for the selection set could be + // changed. + // Note: Rebasing after flattening, since rebasing before that can error out. + // Or, `flatten_unnecessary_fragments` could `rebase` at the same time. + let rebased_selection_set = + selection_set.rebase_on(&rebased_casted_type, named_fragments, schema)?; + Ok(Some( + Selection::InlineFragment(Arc::new(InlineFragmentSelection::new( + rebased_inline_fragment, + rebased_selection_set, + ))) + .into(), + )) + } } } } diff --git a/apollo-federation/src/operation/tests/mod.rs b/apollo-federation/src/operation/tests/mod.rs index 52eb810f7d..6988b8e659 100644 --- a/apollo-federation/src/operation/tests/mod.rs +++ b/apollo-federation/src/operation/tests/mod.rs @@ -4,6 +4,7 @@ use apollo_compiler::schema::Schema; use apollo_compiler::ExecutableDocument; use super::normalize_operation; +use super::Field; use super::Name; use super::NamedFragments; use super::Operation; @@ -1120,13 +1121,13 @@ fn converting_operation_types() { } fn contains_field(ss: &SelectionSet, field_name: Name) -> bool { - ss.selections.contains_key(&SelectionKey::Field { - response_name: field_name, - directives: Default::default(), + ss.selections.contains_key(SelectionKey::Field { + response_name: &field_name, + directives: &Default::default(), }) } -fn is_named_field(sk: &SelectionKey, name: Name) -> bool { +fn is_named_field(sk: SelectionKey, name: Name) -> bool { matches!(sk, SelectionKey::Field { response_name, directives: _ } if *response_name == name) @@ -1137,14 +1138,7 @@ fn get_value_at_path<'a>(ss: &'a SelectionSet, path: &[Name]) -> Option<&'a Sele // Error: empty path return None; }; - let result = ss.selections.get(&SelectionKey::Field { - response_name: (*first).clone(), - directives: Default::default(), - }); - let Some(value) = result else { - // Error: No matching field found. - return None; - }; + let value = ss.selections.get(SelectionKey::field_name(first))?; if rest.is_empty() { // Base case => We are done. Some(value) @@ -1305,14 +1299,14 @@ mod lazy_map_tests { // Remove `foo` let remove_foo = - filter_rec(&selection_set, &|s| !is_named_field(&s.key(), name!("foo"))).unwrap(); + filter_rec(&selection_set, &|s| !is_named_field(s.key(), name!("foo"))).unwrap(); assert!(contains_field(&remove_foo, name!("some_int"))); assert!(contains_field(&remove_foo, name!("foo2"))); assert!(!contains_field(&remove_foo, name!("foo"))); // Remove `bar` let remove_bar = - filter_rec(&selection_set, &|s| !is_named_field(&s.key(), name!("bar"))).unwrap(); + filter_rec(&selection_set, &|s| !is_named_field(s.key(), name!("bar"))).unwrap(); // "foo2" should be removed, since it has no sub-selections left. assert!(!contains_field(&remove_bar, name!("foo2"))); } @@ -1355,7 +1349,7 @@ mod lazy_map_tests { // Add __typename next to any "id" field. let result = - add_typename_if(&selection_set, &|s| is_named_field(&s.key(), name!("id"))).unwrap(); + add_typename_if(&selection_set, &|s| is_named_field(s.key(), name!("id"))).unwrap(); // The top level won't have __typename, since it doesn't have "id". assert!(!contains_field(&result, name!("__typename"))); @@ -1367,7 +1361,7 @@ mod lazy_map_tests { } fn field_element(schema: &ValidFederationSchema, object: Name, field: Name) -> OpPathElement { - OpPathElement::Field(super::Field::new(super::FieldData { + OpPathElement::Field(Field { schema: schema.clone(), field_position: ObjectTypeDefinitionPosition::new(object) .field(field) @@ -1376,7 +1370,7 @@ fn field_element(schema: &ValidFederationSchema, object: Name, field: Name) -> O arguments: Default::default(), directives: Default::default(), sibling_typename: None, - })) + }) } const ADD_AT_PATH_TEST_SCHEMA: &str = r#" @@ -1622,7 +1616,7 @@ fn used_variables() { let Selection::Field(subquery) = operation .selection_set .selections - .get(&SelectionKey::field_name("subquery")) + .get(SelectionKey::field_name(&name!("subquery"))) .unwrap() else { unreachable!(); diff --git a/apollo-federation/src/query_graph/build_query_graph.rs b/apollo-federation/src/query_graph/build_query_graph.rs index fa8da0cec8..81cd361f4f 100644 --- a/apollo-federation/src/query_graph/build_query_graph.rs +++ b/apollo-federation/src/query_graph/build_query_graph.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use apollo_compiler::collections::HashMap; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::schema::DirectiveList as ComponentDirectiveList; @@ -8,12 +7,15 @@ use apollo_compiler::schema::ExtendedType; use apollo_compiler::validation::Valid; use apollo_compiler::Name; use apollo_compiler::Schema; +use itertools::Itertools; use petgraph::graph::EdgeIndex; use petgraph::graph::NodeIndex; use petgraph::visit::EdgeRef; use petgraph::Direction; +use regex::Regex; use strum::IntoEnumIterator; +use crate::bail; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::link::federation_spec_definition::get_federation_spec_definition_from_subgraph; @@ -22,6 +24,7 @@ use crate::link::federation_spec_definition::KeyDirectiveArguments; use crate::operation::merge_selection_sets; use crate::operation::Selection; use crate::operation::SelectionSet; +use crate::query_graph::ContextCondition; use crate::query_graph::OverrideCondition; use crate::query_graph::QueryGraph; use crate::query_graph::QueryGraphEdge; @@ -33,6 +36,7 @@ use crate::schema::position::AbstractTypeDefinitionPosition; use crate::schema::position::CompositeTypeDefinitionPosition; use crate::schema::position::FieldDefinitionPosition; use crate::schema::position::InterfaceTypeDefinitionPosition; +use crate::schema::position::ObjectFieldArgumentDefinitionPosition; use crate::schema::position::ObjectFieldDefinitionPosition; use crate::schema::position::ObjectOrInterfaceTypeDefinitionPosition; use crate::schema::position::ObjectTypeDefinitionPosition; @@ -43,6 +47,7 @@ use crate::schema::position::TypeDefinitionPosition; use crate::schema::position::UnionTypeDefinitionPosition; use crate::schema::ValidFederationSchema; use crate::supergraph::extract_subgraphs_from_supergraph; +use crate::utils::FallibleIterator; /// Builds a "federated" query graph based on the provided supergraph and API schema. /// @@ -59,7 +64,7 @@ pub fn build_federated_query_graph( for_query_planning: Option, ) -> Result { let for_query_planning = for_query_planning.unwrap_or(true); - let mut query_graph = QueryGraph { + let query_graph = QueryGraph { // Note this name is a dummy initial name that gets overridden as we build the query graph. current_source: "".into(), graph: Default::default(), @@ -68,22 +73,23 @@ pub fn build_federated_query_graph( types_to_nodes_by_source: Default::default(), root_kinds_to_nodes_by_source: Default::default(), non_trivial_followup_edges: Default::default(), + subgraph_to_args: Default::default(), + subgraph_to_arg_indices: Default::default(), }; - let subgraphs = - extract_subgraphs_from_supergraph(&supergraph_schema, validate_extracted_subgraphs)?; - for (subgraph_name, subgraph) in subgraphs { - let builder = SchemaQueryGraphBuilder::new( - query_graph, - subgraph_name, - subgraph.schema, - Some(api_schema.clone()), - for_query_planning, - )?; - query_graph = builder.build()?; - } - let federated_builder = FederatedQueryGraphBuilder::new(query_graph, supergraph_schema)?; - query_graph = federated_builder.build()?; - Ok(query_graph) + let query_graph = + extract_subgraphs_from_supergraph(&supergraph_schema, validate_extracted_subgraphs)? + .into_iter() + .fallible_fold(query_graph, |query_graph, (subgraph_name, subgraph)| { + SchemaQueryGraphBuilder::new( + query_graph, + subgraph_name, + subgraph.schema, + Some(api_schema.clone()), + for_query_planning, + )? + .build() + })?; + FederatedQueryGraphBuilder::new(query_graph, supergraph_schema)?.build() } /// Builds a query graph based on the provided schema (usually an API schema outside of testing). @@ -102,6 +108,8 @@ pub fn build_query_graph( types_to_nodes_by_source: Default::default(), root_kinds_to_nodes_by_source: Default::default(), non_trivial_followup_edges: Default::default(), + subgraph_to_args: Default::default(), + subgraph_to_arg_indices: Default::default(), }; let builder = SchemaQueryGraphBuilder::new(query_graph, name, schema, None, false)?; query_graph = builder.build()?; @@ -136,15 +144,9 @@ impl BaseQueryGraphBuilder { transition: QueryGraphEdgeTransition, conditions: Option>, ) -> Result<(), FederationError> { - self.query_graph.graph.add_edge( - head, - tail, - QueryGraphEdge { - transition, - conditions, - override_condition: None, - }, - ); + self.query_graph + .graph + .add_edge(head, tail, QueryGraphEdge::new(transition, conditions)); let head_weight = self.query_graph.node_weight(head)?; let tail_weight = self.query_graph.node_weight(tail)?; if head_weight.source != tail_weight.source { @@ -986,6 +988,7 @@ impl FederatedQueryGraphBuilder { self.handle_key()?; self.handle_requires()?; self.handle_progressive_overrides()?; + self.handle_context()?; // Note that @provides must be handled last when building since it requires copying nodes // and their edges, and it's easier to reason about this if we know previous self.handle_provides()?; @@ -1011,15 +1014,14 @@ impl FederatedQueryGraphBuilder { } fn add_federated_root_nodes(&mut self) -> Result<(), FederationError> { - let mut root_kinds = IndexSet::default(); - for (source, root_kinds_to_nodes) in &self.base.query_graph.root_kinds_to_nodes_by_source { - if *source == self.base.query_graph.current_source { - continue; - } - for root_kind in root_kinds_to_nodes.keys() { - root_kinds.insert(*root_kind); - } - } + let root_kinds = self + .base + .query_graph + .root_kinds_to_nodes_by_source + .iter() + .filter(|(source, _)| **source != self.base.query_graph.current_source) + .flat_map(|(_, root_kind_to_nodes)| root_kind_to_nodes.keys().copied()) + .collect::>(); for root_kind in root_kinds { self.base.create_root_node(root_kind.into(), root_kind)?; } @@ -1385,7 +1387,7 @@ impl FederatedQueryGraphBuilder { /// override condition of `false`, whereas the "to" subgraph will have an /// override condition of `true`. fn handle_progressive_overrides(&mut self) -> Result<(), FederationError> { - let mut edge_to_conditions: HashMap = Default::default(); + let mut edge_to_conditions: IndexMap = Default::default(); fn collect_edge_condition( query_graph: &QueryGraph, @@ -1393,7 +1395,7 @@ impl FederatedQueryGraphBuilder { target_field: &ObjectFieldDefinitionPosition, label: &str, condition: bool, - edge_to_conditions: &mut HashMap, + edge_to_conditions: &mut IndexMap, ) -> Result<(), FederationError> { let target_field = FieldDefinitionPosition::Object(target_field.clone()); let subgraph_nodes = query_graph @@ -1474,6 +1476,173 @@ impl FederatedQueryGraphBuilder { Ok(()) } + fn handle_context(&mut self) -> Result<(), FederationError> { + let mut subgraph_to_args: IndexMap, Vec> = + IndexMap::default(); + let mut coordinate_map: IndexMap< + Arc, + IndexMap>, + > = IndexMap::default(); + for (subgraph_name, subgraph) in self.base.query_graph.subgraphs() { + let subgraph_data = self.subgraphs.get(subgraph_name)?; + let Some((_, context_refs)) = &subgraph + .referencers() + .directives + .iter() + .find(|(dir, _)| **dir == subgraph_data.context_directive_definition_name) + else { + continue; + }; + + let Some((_, from_context_refs)) = &subgraph + .referencers() + .directives + .iter() + .find(|(dir, _)| **dir == subgraph_data.from_context_directive_definition_name) + else { + continue; + }; + + // Collect data for @context + let mut context_name_to_types: IndexMap< + &str, + IndexSet, + > = Default::default(); + for object_def_pos in &context_refs.object_types { + let object = object_def_pos.get(subgraph.schema())?; + for dir in object + .directives + .get_all(subgraph_data.context_directive_definition_name.as_str()) + { + let application = FederationSpecDefinition::context_directive_arguments(dir)?; + context_name_to_types + .entry(application.name) + .or_default() + .insert(object_def_pos.clone().into()); + } + } + for interface_def_pos in &context_refs.interface_types { + let interface = interface_def_pos.get(subgraph.schema())?; + for dir in interface + .directives + .get_all(subgraph_data.context_directive_definition_name.as_str()) + { + let application = FederationSpecDefinition::context_directive_arguments(dir)?; + context_name_to_types + .entry(application.name) + .or_default() + .insert(interface_def_pos.clone().into()); + } + } + for union_def_pos in &context_refs.union_types { + let union = union_def_pos.get(subgraph.schema())?; + for dir in union + .directives + .get_all(subgraph_data.context_directive_definition_name.as_str()) + { + let application = FederationSpecDefinition::context_directive_arguments(dir)?; + context_name_to_types + .entry(application.name) + .or_default() + .insert(union_def_pos.clone().into()); + } + } + + // Collect data for @fromContext + let coordinate_map = coordinate_map.entry(subgraph_name.clone()).or_default(); + for object_field_arg in &from_context_refs.object_field_arguments { + let input_value = object_field_arg.get(subgraph.schema())?; + subgraph_to_args + .entry(subgraph_name.clone()) + .or_default() + .push(object_field_arg.clone()); + let field_coordinate = object_field_arg.parent(); + if let Some(dir) = input_value.directives.get( + subgraph_data + .from_context_directive_definition_name + .as_str(), + ) { + let application = subgraph_data + .federation_spec_definition + .from_context_directive_arguments(dir)?; + let (context, selection) = parse_context(application.field)?; + + let types_with_context_set = context_name_to_types + .get(context.as_str()) + .into_iter() + .flatten() + .cloned() + .collect(); + let conditions = ContextCondition { + context, + selection, + subgraph_name: subgraph_name.clone(), + argument_coordinate: object_field_arg.clone(), + types_with_context_set, + named_parameter: object_field_arg.argument_name.to_owned(), + arg_type: input_value.ty.clone(), + }; + coordinate_map + .entry(field_coordinate.clone()) + .or_default() + .push(conditions); + } + } + } + + for edge in self.base.query_graph.graph.edge_indices() { + let edge_weight = self.base.query_graph.edge_weight(edge)?; + let QueryGraphEdgeTransition::FieldCollection { + source, + field_definition_position, + .. + } = &edge_weight.transition + else { + continue; + }; + let FieldDefinitionPosition::Object(obj_field) = field_definition_position else { + continue; + }; + let Some(contexts) = coordinate_map.get_mut(source) else { + continue; + }; + let Some(required_contexts) = contexts.get(obj_field) else { + continue; + }; + self.base + .query_graph + .edge_weight_mut(edge)? + .required_contexts + .extend_from_slice(required_contexts); + } + + // Add the context argument mapping + self.base.query_graph.subgraph_to_arg_indices = self + .base + .query_graph + .subgraphs() + .filter_map(|(source, _)| subgraph_to_args.get_full(source)) + .map(|(index, source, args)| { + Ok::<_, FederationError>(( + source.clone(), + args.iter() + .sorted() + .enumerate() + .map(|(i, arg)| { + Ok::<_, FederationError>(( + arg.clone(), + format!("contextualArgument_{}_{}", index + 1, i).try_into()?, + )) + }) + .process_results(|r| r.collect())?, + )) + }) + .process_results(|r| r.collect())?; + self.base.query_graph.subgraph_to_args = subgraph_to_args; + + Ok(()) + } + /// Handle @provides by copying the appropriate nodes/edges. fn handle_provides(&mut self) -> Result<(), FederationError> { let mut provide_id = 0; @@ -2083,6 +2252,14 @@ impl FederatedQueryGraphBuilderSubgraphs { .override_directive_definition(schema)? .name .clone(); + let context_directive_definition_name = federation_spec_definition + .context_directive_definition(schema)? + .name + .clone(); + let from_context_directive_definition_name = federation_spec_definition + .from_context_directive_definition(schema)? + .name + .clone(); subgraphs.map.insert( source.clone(), FederatedQueryGraphBuilderSubgraphData { @@ -2092,6 +2269,8 @@ impl FederatedQueryGraphBuilderSubgraphs { provides_directive_definition_name, interface_object_directive_definition_name, overrides_directive_definition_name, + context_directive_definition_name, + from_context_directive_definition_name, }, ); } @@ -2118,6 +2297,8 @@ struct FederatedQueryGraphBuilderSubgraphData { provides_directive_definition_name: Name, interface_object_directive_definition_name: Name, overrides_directive_definition_name: Name, + context_directive_definition_name: Name, + from_context_directive_definition_name: Name, } #[derive(Debug)] @@ -2151,6 +2332,45 @@ fn resolvable_key_applications<'doc>( Ok(applications) } +fn parse_context(field: &str) -> Result<(String, String), FederationError> { + let pattern = Regex::new( + r#"^(?:[\n\r\t ,]|#[^\n\r]*)*\$(?:[\n\r\t ,]|#[^\n\r]*)*([A-Za-z_]\w*)([\s\S]*)$"#, + ) + .unwrap(); + + let mut iter = pattern.captures_iter(field); + + let Some(captures) = iter.next() else { + bail!("Expected to find the name of a context and a selection inside the field argument to `@fromContext`: {field:?}"); + }; + + if iter.next().is_some() { + bail!("Expected only one context and selection pair inside the field argument to `@fromContext`: {field:?}"); + } + + let (context, selection) = captures + .iter() + // Ignore the first match because it is always the whole matching substring + .skip(1) + .flatten() + .fold(("", ""), |(_, b), group| (b, group.as_str())); + + if context.is_empty() { + bail!("Expected to find the name of a context inside the field argument to `@fromContext`: {field:?}"); + } + + if selection.is_empty() { + bail!( + "Expected to find and a selection inside the field argument to `@fromContext`: {field:?}" + ); + } + + Ok(( + context.trim_end().to_owned(), + selection.trim_start().to_owned(), + )) +} + #[cfg(test)] mod tests { use apollo_compiler::collections::IndexMap; @@ -2253,6 +2473,43 @@ mod tests { Ok(tails.pop().unwrap()) } + #[test] + fn test_parse_context() { + let fields = [ + ("$context { prop }", ("context", "{ prop }")), + ( + "$context ... on A { prop } ... on B { prop }", + ("context", "... on A { prop } ... on B { prop }"), + ), + ( + "$topLevelQuery { me { locale } }", + ("topLevelQuery", "{ me { locale } }"), + ), + ( + "$context { a { b { c { prop }}} }", + ("context", "{ a { b { c { prop }}} }"), + ), + ( + "$ctx { identifiers { legacyUserId } }", + ("ctx", "{ identifiers { legacyUserId } }"), + ), + ( + "$retailCtx { identifiers { id5 } }", + ("retailCtx", "{ identifiers { id5 } }"), + ), + ("$retailCtx { mid }", ("retailCtx", "{ mid }")), + ( + "$widCtx { identifiers { wid } }", + ("widCtx", "{ identifiers { wid } }"), + ), + ]; + for (field, (known_context, known_selection)) in fields { + let (context, selection) = super::parse_context(field).unwrap(); + assert_eq!(context, known_context); + assert_eq!(selection, known_selection); + } + } + #[test] fn building_query_graphs_from_schema_handles_object_types() -> Result<(), FederationError> { let query_graph = test_query_graph_from_schema_sdl( diff --git a/apollo-federation/src/query_graph/condition_resolver.rs b/apollo-federation/src/query_graph/condition_resolver.rs index e547f2a235..c9f3b0da66 100644 --- a/apollo-federation/src/query_graph/condition_resolver.rs +++ b/apollo-federation/src/query_graph/condition_resolver.rs @@ -3,16 +3,31 @@ // trait directly using `ConditionResolverCache`. use std::sync::Arc; +use apollo_compiler::ast::Type; use apollo_compiler::collections::IndexMap; +use apollo_compiler::Name; +use apollo_compiler::Node; use petgraph::graph::EdgeIndex; use crate::error::FederationError; +use crate::operation::SelectionSet; use crate::query_graph::graph_path::ExcludedConditions; use crate::query_graph::graph_path::ExcludedDestinations; use crate::query_graph::graph_path::OpGraphPathContext; use crate::query_graph::path_tree::OpPathTree; use crate::query_plan::QueryPlanCost; +#[derive(Debug, Clone)] +pub(crate) struct ContextMapEntry { + pub(crate) levels_in_data_path: usize, + pub(crate) levels_in_query_path: usize, + pub(crate) path_tree: Option>, + pub(crate) selection_set: SelectionSet, + pub(crate) param_name: Name, + pub(crate) arg_type: Node, + pub(crate) id: Name, +} + /// Note that `ConditionResolver`s are guaranteed to be only called for edge with conditions. pub(crate) trait ConditionResolver { fn resolve( @@ -21,6 +36,7 @@ pub(crate) trait ConditionResolver { context: &OpGraphPathContext, excluded_destinations: &ExcludedDestinations, excluded_conditions: &ExcludedConditions, + extra_conditions: Option<&SelectionSet>, ) -> Result; } @@ -29,6 +45,7 @@ pub(crate) enum ConditionResolution { Satisfied { cost: QueryPlanCost, path_tree: Option>, + context_map: Option>, }, Unsatisfied { // NOTE: This seems to be a false positive... @@ -40,6 +57,7 @@ pub(crate) enum ConditionResolution { #[derive(Debug, Clone)] pub(crate) enum UnsatisfiedConditionReason { NoPostRequireKey, + NoSetContext, } impl ConditionResolution { @@ -47,6 +65,7 @@ impl ConditionResolution { Self::Satisfied { cost: 0.0, path_tree: None, + context_map: None, } } @@ -91,7 +110,11 @@ impl ConditionResolverCache { context: &OpGraphPathContext, excluded_destinations: &ExcludedDestinations, excluded_conditions: &ExcludedConditions, + extra_conditions: Option<&SelectionSet>, ) -> ConditionResolutionCacheResult { + if extra_conditions.is_some() { + return ConditionResolutionCacheResult::NotApplicable; + } // We don't cache if there is a context or excluded conditions because those would impact the resolution and // we don't want to cache a value per-context and per-excluded-conditions (we also don't cache per-excluded-edges though // instead we cache a value only for the first-see excluded edges; see above why that work in practice). @@ -150,7 +173,8 @@ mod tests { edge1, &empty_context, &empty_destinations, - &empty_conditions + &empty_conditions, + None ) .is_miss()); @@ -165,7 +189,8 @@ mod tests { edge1, &empty_context, &empty_destinations, - &empty_conditions + &empty_conditions, + None ) .is_hit(),); @@ -176,7 +201,8 @@ mod tests { edge2, &empty_context, &empty_destinations, - &empty_conditions + &empty_conditions, + None ) .is_miss()); } diff --git a/apollo-federation/src/query_graph/graph_path.rs b/apollo-federation/src/query_graph/graph_path.rs index 7c9fe75827..3bf55adc7d 100644 --- a/apollo-federation/src/query_graph/graph_path.rs +++ b/apollo-federation/src/query_graph/graph_path.rs @@ -4,12 +4,18 @@ use std::fmt::Formatter; use std::fmt::Write; use std::hash::Hash; use std::ops::Deref; +use std::ops::DerefMut; use std::sync::atomic; use std::sync::Arc; +use apollo_compiler::ast::InputValueDefinition; +use apollo_compiler::ast::Type; use apollo_compiler::ast::Value; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; +use apollo_compiler::executable::Argument; +use apollo_compiler::Name; +use apollo_compiler::Node; use either::Either; use itertools::Itertools; use petgraph::graph::EdgeIndex; @@ -19,11 +25,14 @@ use petgraph::visit::EdgeRef; use tracing::debug; use tracing::debug_span; +use super::condition_resolver::ContextMapEntry; +use crate::bail; use crate::display_helpers::write_indented_lines; use crate::display_helpers::DisplayOption; use crate::display_helpers::DisplaySlice; use crate::display_helpers::State as IndentedFormatter; use crate::error::FederationError; +use crate::internal_error; use crate::is_leaf_type; use crate::link::federation_spec_definition::get_federation_spec_definition_from_subgraph; use crate::link::graphql_definition::BooleanOrVariable; @@ -32,12 +41,12 @@ use crate::link::graphql_definition::OperationConditional; use crate::link::graphql_definition::OperationConditionalKind; use crate::operation::DirectiveList; use crate::operation::Field; -use crate::operation::FieldData; use crate::operation::HasSelectionKey; use crate::operation::InlineFragment; -use crate::operation::InlineFragmentData; +use crate::operation::NamedFragments; use crate::operation::SelectionId; use crate::operation::SelectionKey; +use crate::operation::SelectionMapperReturn; use crate::operation::SelectionSet; use crate::operation::SiblingTypename; use crate::query_graph::condition_resolver::ConditionResolution; @@ -50,9 +59,11 @@ use crate::query_graph::QueryGraphNodeType; use crate::query_plan::query_planner::EnabledOverrideConditions; use crate::query_plan::FetchDataPathElement; use crate::query_plan::QueryPlanCost; +use crate::schema::field_set::parse_field_set; use crate::schema::position::AbstractTypeDefinitionPosition; use crate::schema::position::Captures; use crate::schema::position::CompositeTypeDefinitionPosition; +use crate::schema::position::FieldDefinitionPosition; use crate::schema::position::InterfaceFieldDefinitionPosition; use crate::schema::position::ObjectOrInterfaceTypeDefinitionPosition; use crate::schema::position::ObjectTypeDefinitionPosition; @@ -60,6 +71,14 @@ use crate::schema::position::OutputTypeDefinitionPosition; use crate::schema::position::TypeDefinitionPosition; use crate::schema::ValidFederationSchema; +#[derive(Clone, serde::Serialize, Debug, Eq, PartialEq)] +pub(crate) struct ContextAtUsageEntry { + pub(crate) context_id: Name, + pub(crate) relative_path: Vec, + pub(crate) selection_set: SelectionSet, + pub(crate) subgraph_arg_type: Node, +} + /// An immutable path in a query graph. /// /// A "path" here is mostly understood in the graph-theoretical sense of the term, i.e. as "a @@ -159,6 +178,11 @@ where // TODO(@TylerBloom): Add in once defer is supported. #[serde(skip)] defer_on_tail: Option, + /// At the point where a `@context` is set, we will have fields to select + context_to_selection: Vec>, + /// Where a context is used (i.e. `@fromContext`) there will exist a ContextAtUsageEntry map + /// (1 for each parameter) + parameter_to_context: Vec>, } impl std::fmt::Debug for GraphPath @@ -185,6 +209,8 @@ where runtime_types_of_tail, runtime_types_before_tail_if_last_is_cast, defer_on_tail, + context_to_selection: _, + parameter_to_context: _, } = self; f.debug_struct("GraphPath") @@ -244,9 +270,17 @@ impl OverrideId { } } +pub(crate) type ContextToSelection = IndexSet; +pub(crate) type ParameterToContext = IndexMap; + /// The item type for [`GraphPath::iter`] -pub(crate) type GraphPathItem<'path, TTrigger, TEdge> = - (TEdge, &'path Arc, &'path Option>); +pub(crate) type GraphPathItem<'path, TTrigger, TEdge> = ( + TEdge, + &'path Arc, + &'path Option>, + Option, + Option, +); /// A `GraphPath` whose triggers are operation elements (essentially meaning that the path has been /// guided by a GraphQL operation). @@ -591,6 +625,12 @@ pub(crate) struct SimultaneousPathsWithLazyIndirectPaths { pub(crate) lazily_computed_indirect_paths: Vec>, } +impl Display for SimultaneousPathsWithLazyIndirectPaths { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.paths) + } +} + /// A "set" of excluded destinations (i.e. subgraph names). Note that we use a `Vec` instead of set /// because this is used in pretty hot paths (the whole path computation is CPU intensive) and will /// basically always be tiny (it's bounded by the number of distinct key on a given type, so usually @@ -850,9 +890,123 @@ where } } +impl TryFrom for Arc { + type Error = FederationError; + + fn try_from(value: GraphPathTrigger) -> Result { + match value { + GraphPathTrigger::Op(op) => Ok(op), + GraphPathTrigger::Transition(_) => { + bail!("Failed to convert to GraphPathTrigger") + } + } + } +} + +impl TryFrom for Arc { + type Error = FederationError; + + fn try_from(value: GraphPathTrigger) -> Result { + match value { + GraphPathTrigger::Transition(transition) => Ok(transition), + GraphPathTrigger::Op(_) => Err(FederationError::internal( + "Failed to convert to GraphPathTrigger", + )), + } + } +} + +pub(crate) enum GraphPathTriggerRef<'a> { + Op(&'a OpGraphPathTrigger), + Transition(&'a QueryGraphEdgeTransition), +} + +pub(crate) enum GraphPathTriggerRefMut<'a> { + Op(&'a mut OpGraphPathTrigger), + Transition(&'a mut QueryGraphEdgeTransition), +} + +impl<'a> From<&'a GraphPathTrigger> for GraphPathTriggerRef<'a> { + fn from(value: &'a GraphPathTrigger) -> Self { + match value { + GraphPathTrigger::Op(value) => value.as_ref().into(), + GraphPathTrigger::Transition(value) => value.as_ref().into(), + } + } +} + +impl<'a> From<&'a OpGraphPathTrigger> for GraphPathTriggerRef<'a> { + fn from(value: &'a OpGraphPathTrigger) -> Self { + Self::Op(value) + } +} + +impl<'a> From<&'a mut OpGraphPathTrigger> for GraphPathTriggerRefMut<'a> { + fn from(value: &'a mut OpGraphPathTrigger) -> Self { + Self::Op(value) + } +} + +impl<'a> From<&'a QueryGraphEdgeTransition> for GraphPathTriggerRef<'a> { + fn from(value: &'a QueryGraphEdgeTransition) -> Self { + Self::Transition(value) + } +} + +impl<'a> From<&'a mut QueryGraphEdgeTransition> for GraphPathTriggerRefMut<'a> { + fn from(value: &'a mut QueryGraphEdgeTransition) -> Self { + Self::Transition(value) + } +} + +/// `GraphPath` is generic over two type, `TTrigger` and `TEdge`. This trait helps abstract over +/// the `TTrigger` type bound. A `TTrigger` is one of the two types that make up the variants of +/// the `GraphPathTrigger`. Rather than trying to cast into concrete types and cast back (and +/// potentially raise errors), this trait provides ways to access the data needed within. +pub(crate) trait GraphPathTriggerVariant: Eq + Hash + std::fmt::Debug { + fn get_field_mut<'a>(&'a mut self) -> Option<&mut Field> + where + &'a mut Self: Into>, + { + match self.into() { + GraphPathTriggerRefMut::Op(OpGraphPathTrigger::OpPathElement( + OpPathElement::Field(field), + )) => Some(field), + _ => None, + } + } + + fn get_field_parent_type<'a>(&'a self) -> Option + where + &'a Self: Into>, + { + match self.into() { + GraphPathTriggerRef::Op(trigger) => match trigger { + OpGraphPathTrigger::OpPathElement(OpPathElement::Field(field)) => { + Some(field.field_position.clone()) + } + _ => None, + }, + GraphPathTriggerRef::Transition(trigger) => match trigger { + QueryGraphEdgeTransition::FieldCollection { + field_definition_position, + .. + } => Some(field_definition_position.clone()), + _ => None, + }, + } + } +} + +impl GraphPathTriggerVariant for OpGraphPathTrigger {} + +impl GraphPathTriggerVariant for QueryGraphEdgeTransition {} + impl GraphPath where - TTrigger: Eq + Hash + std::fmt::Debug, + TTrigger: GraphPathTriggerVariant, + for<'a> &'a TTrigger: Into>, + for<'a> &'a mut TTrigger: Into>, Arc: Into, TEdge: Copy + Into> + std::fmt::Debug, EdgeIndex: Into, @@ -871,6 +1025,8 @@ where runtime_types_of_tail: Arc::new(IndexSet::default()), runtime_types_before_tail_if_last_is_cast: None, defer_on_tail: None, + context_to_selection: Vec::default(), + parameter_to_context: Vec::default(), }; path.runtime_types_of_tail = Arc::new(path.head_possible_runtime_types()?); Ok(path) @@ -894,7 +1050,7 @@ where pub(crate) fn add( &self, - trigger: TTrigger, + mut trigger: TTrigger, edge: TEdge, condition_resolution: ConditionResolution, defer: Option, @@ -902,6 +1058,7 @@ where let ConditionResolution::Satisfied { path_tree: condition_path_tree, cost: condition_cost, + context_map, } = condition_resolution else { return Err(FederationError::internal( @@ -912,6 +1069,8 @@ where let mut edges = self.edges.clone(); let mut edge_triggers = self.edge_triggers.clone(); let mut edge_conditions = self.edge_conditions.clone(); + let mut context_to_selection = self.context_to_selection.clone(); + let mut parameter_to_context = self.parameter_to_context.clone(); let mut last_subgraph_entering_edge_info = if defer.is_none() { self.last_subgraph_entering_edge_info.clone() } else { @@ -922,6 +1081,8 @@ where edges.push(edge); edge_triggers.push(Arc::new(trigger)); edge_conditions.push(condition_path_tree); + context_to_selection.push(None); + parameter_to_context.push(None); return Ok(GraphPath { graph: self.graph.clone(), head: self.head, @@ -941,6 +1102,8 @@ where ), runtime_types_before_tail_if_last_is_cast: None, defer_on_tail: defer, + context_to_selection, + parameter_to_context, }); }; @@ -1078,6 +1241,8 @@ where } else { self.defer_on_tail.clone() }, + context_to_selection: self.context_to_selection.clone(), + parameter_to_context: self.parameter_to_context.clone(), }); } } @@ -1138,13 +1303,72 @@ where // We know last edge is not a cast. runtime_types_before_tail_if_last_is_cast: None, defer_on_tail: defer, + context_to_selection: self.context_to_selection.clone(), + parameter_to_context: self.parameter_to_context.clone(), }); } } + let (new_edge_conditions, new_context_to_selection, new_parameter_to_context) = + self.merge_edge_conditions_with_resolution(&condition_path_tree, &context_map); + let last_parameter_to_context = new_parameter_to_context.last(); + + if let Some(Some(last_parameter_to_context)) = last_parameter_to_context { + // TODO: Perhaps it is better to explicitly cast this to `GraphPathTriggerRefMut` and + // pull out the field from there. + if let Some(field) = trigger.get_field_mut() { + let mut schema = field.schema.schema().clone().into_inner(); + let type_name = field.field_position.type_name(); + let field_name = field.field_position.field_name(); + let Some(field_def) = schema.types.get_mut(type_name).and_then(|t| match t { + apollo_compiler::schema::ExtendedType::Scalar(_) => None, + apollo_compiler::schema::ExtendedType::Object(obj) => { + obj.make_mut().fields.get_mut(field_name) + } + apollo_compiler::schema::ExtendedType::Interface(iface) => { + iface.make_mut().fields.get_mut(field_name) + } + apollo_compiler::schema::ExtendedType::Union(_) => None, + apollo_compiler::schema::ExtendedType::Enum(_) => None, + apollo_compiler::schema::ExtendedType::InputObject(_) => None, + }) else { + bail!("Unexpectedly failed to lookup field {type_name}.{field_name}") + }; + let field_def = field_def.deref_mut().make_mut(); + let mut updated_field_arguments = vec![]; + let mut updated_field_def_arguments = vec![]; + for (param_name, usage_entry) in last_parameter_to_context { + if !field_def + .arguments + .iter() + .any(|arg| arg.name.as_str() == param_name.as_str()) + { + updated_field_def_arguments.push(Node::new(InputValueDefinition { + name: usage_entry.context_id.clone(), + ty: usage_entry.subgraph_arg_type.clone(), + default_value: None, + description: None, + directives: Default::default(), + })); + updated_field_arguments.push(Node::new(Argument { + name: param_name.clone(), + value: Node::new(Value::Variable(usage_entry.context_id.clone())), + })); + } + } + field_def.arguments.extend(updated_field_def_arguments); + field.schema = ValidFederationSchema::new(schema.validate()?)?; + field.arguments = field + .arguments + .iter() + .cloned() + .chain(updated_field_arguments) + .collect(); + } + } + edges.push(edge); edge_triggers.push(Arc::new(trigger)); - edge_conditions.push(condition_path_tree); if defer.is_none() && self.graph.is_cross_subgraph_edge(new_edge)? { last_subgraph_entering_edge_info = Some(SubgraphEnteringEdgeInfo { index: self.edges.len(), @@ -1157,7 +1381,7 @@ where tail: edge_tail, edges, edge_triggers, - edge_conditions, + edge_conditions: new_edge_conditions, // Again, we don't want to set `last_subgraph_entering_edge_info` if we're entering a // `@defer` (see above). last_subgraph_entering_edge_info, @@ -1189,18 +1413,88 @@ where } else { None }, + context_to_selection: new_context_to_selection, + parameter_to_context: new_parameter_to_context, }) } + #[allow(clippy::type_complexity)] + fn merge_edge_conditions_with_resolution( + &self, + condition_path_tree: &Option>, + context_map: &Option>, + ) -> ( + Vec>>, + Vec>>, + Vec>>, + ) { + let mut edge_conditions = self.edge_conditions.clone(); + let mut context_to_selection = self.context_to_selection.clone(); + let mut parameter_to_context = self.parameter_to_context.clone(); + + edge_conditions.push(condition_path_tree.clone()); + if context_map.is_none() || context_map.as_ref().is_some_and(|m| m.is_empty()) { + context_to_selection.push(None); + parameter_to_context.push(None); + (edge_conditions, context_to_selection, parameter_to_context) + } else { + // parameter_to_context.push(Some(Arc::new(IndexMap::default()))); + context_to_selection.push(None); + let mut new_parameter_to_context = IndexMap::default(); + for (_, entry) in context_map.iter().flat_map(|map| map.iter()) { + let idx = edge_conditions.len() - entry.levels_in_query_path - 1; + + if let Some(path_tree) = &entry.path_tree { + let merged_conditions = edge_conditions[idx] + .as_ref() + .map_or_else(|| path_tree.clone(), |condition| condition.merge(path_tree)); + edge_conditions[idx] = Some(merged_conditions); + } + context_to_selection[idx] + .get_or_insert_with(Default::default) + .insert(entry.id.clone()); + + new_parameter_to_context.insert( + entry.param_name.clone(), + ContextAtUsageEntry { + context_id: entry.id.clone(), + relative_path: vec![ + FetchDataPathElement::Parent; + entry.levels_in_data_path + ], + selection_set: entry.selection_set.clone(), + subgraph_arg_type: entry.arg_type.clone(), + }, + ); + } + parameter_to_context.push(Some(new_parameter_to_context)); + (edge_conditions, context_to_selection, parameter_to_context) + } + } + pub(crate) fn iter(&self) -> impl Iterator> { debug_assert_eq!(self.edges.len(), self.edge_triggers.len()); debug_assert_eq!(self.edges.len(), self.edge_conditions.len()); + debug_assert_eq!(self.edges.len(), self.context_to_selection.len()); + debug_assert_eq!(self.edges.len(), self.parameter_to_context.len()); self.edges .iter() .copied() .zip(&self.edge_triggers) .zip(&self.edge_conditions) - .map(|((edge, trigger), condition)| (edge, trigger, condition)) + .zip(&self.context_to_selection) + .zip(&self.parameter_to_context) + .map( + |((((edge, trigger), condition), context_to_selection), parameter_to_context)| { + ( + edge, + trigger, + condition, + context_to_selection.clone(), + parameter_to_context.clone(), + ) + }, + ) } pub(crate) fn next_edges( @@ -1351,10 +1645,12 @@ where == Some(self.edges.len() - 2)) } + /* #[cfg_attr( feature = "snapshot_tracing", tracing::instrument(skip_all, level = "trace", name = "GraphPath::can_satisfy_conditions") )] + */ fn can_satisfy_conditions( &self, edge: EdgeIndex, @@ -1364,15 +1660,210 @@ where excluded_conditions: &ExcludedConditions, ) -> Result { let edge_weight = self.graph.edge_weight(edge)?; - if edge_weight.conditions.is_none() { + if edge_weight.conditions.is_none() && edge_weight.required_contexts.is_empty() { return Ok(ConditionResolution::no_conditions()); } + + /* Resolve context conditions */ + + let mut total_cost = 0.; + let mut context_map: IndexMap = IndexMap::default(); + + if !edge_weight.required_contexts.is_empty() { + let mut was_unsatisfied = false; + for ctx in &edge_weight.required_contexts { + let mut levels_in_data_path = 0; + for (mut levels_in_query_path, (e, trigger)) in self + .edges + .iter() + .zip(self.edge_triggers.iter()) + .rev() + .enumerate() + { + let parent_type = trigger.get_field_parent_type(); + levels_in_query_path += 1; + if parent_type.is_some() { + levels_in_data_path += 1; + } + let Some(e) = (*e).into() else { continue }; + if !was_unsatisfied && !context_map.contains_key(&ctx.named_parameter) { + if let Some(parent_type) = parent_type { + let parent_type = parent_type.parent(); + let subgraph_schema = + self.graph.schema_by_source(&ctx.subgraph_name)?; + let mut potential_matches: IndexSet = Default::default(); + ctx.types_with_context_set.iter().for_each(|pos| { + if pos.type_name() == parent_type.type_name() { + potential_matches.insert(parent_type.type_name().clone()); + } + match &pos { + CompositeTypeDefinitionPosition::Object(obj_pos) => { + if let Ok(obj) = obj_pos.get(subgraph_schema.schema()) { + obj.implements_interfaces + .iter() + .filter(|item| { + &item.name == parent_type.type_name() + }) + .for_each(|item| { + potential_matches.insert(item.name.clone()); + }); + } + } + CompositeTypeDefinitionPosition::Interface(iface_pos) => { + if let Ok(iface) = iface_pos.get(subgraph_schema.schema()) { + iface + .implements_interfaces + .iter() + .filter(|item| { + &item.name == parent_type.type_name() + }) + .for_each(|_| { + potential_matches.insert(iface.name.clone()); + }); + } + } + CompositeTypeDefinitionPosition::Union(union_pos) => { + if let Ok(un) = union_pos.get(subgraph_schema.schema()) { + un.members + .iter() + .filter(|item| { + &item.name == parent_type.type_name() + }) + .for_each(|_| { + potential_matches.insert(un.name.clone()); + }); + } + } + } + }); + + // get the selection set for the first match that parses + let selection_set = + potential_matches.iter().find_map(|parent_type_name| { + parse_field_set( + subgraph_schema, + parent_type_name.clone(), + &ctx.selection, + ) + .ok() + }); + + if let Some(selection_set) = selection_set { + selection_set.lazy_map( + &NamedFragments::default(), + |selection| { + if let OpPathElement::InlineFragment(fragment) = + selection.element()? + { + if let Some(CompositeTypeDefinitionPosition::Object( + obj, + )) = &fragment.type_condition_position + { + if !subgraph_schema + .possible_runtime_types(parent_type.clone())? + .contains(obj) + { + return Ok(SelectionMapperReturn::None); + } + } + } + Ok(SelectionMapperReturn::Selection(selection.clone())) + }, + )?; + let resolution = condition_resolver.resolve( + e, + context, + excluded_destinations, + excluded_conditions, + Some(&selection_set), + )?; + let Some(arg_indices) = + self.graph.subgraph_to_arg_indices.get(&ctx.subgraph_name) + else { + bail!("Unknown subgraph, {:?}, in QueryGraph::subgraph_to_arg_indices", ctx.subgraph_name) + }; + let Some(id) = arg_indices.get(&ctx.argument_coordinate).cloned() + else { + bail!("Unknown argument coordiate, {:?}, in QueryGraph::subgraph_to_arg_indices[{}]", ctx.argument_coordinate, ctx.subgraph_name) + }; + + match &resolution { + ConditionResolution::Satisfied { + cost, path_tree, .. + } => { + total_cost += cost; + let entry = ContextMapEntry { + levels_in_data_path, + levels_in_query_path, + path_tree: path_tree.clone(), + selection_set, + param_name: ctx.named_parameter.clone(), + arg_type: ctx.arg_type.clone(), + id, + }; + context_map.insert(ctx.named_parameter.clone(), entry); + } + ConditionResolution::Unsatisfied { .. } => { + was_unsatisfied = true + } + } + } else { + internal_error!( + "Could not parse selection {} over any types {:?}", + &ctx.selection, + potential_matches + ); + } + } + } + } + } + + if edge_weight + .required_contexts + .iter() + .any(|ctx| !context_map.contains_key(&ctx.named_parameter)) + { + debug!("@fromContext requires a context that is not set in graph path"); + return Ok(ConditionResolution::Unsatisfied { + reason: Some(UnsatisfiedConditionReason::NoSetContext), + }); + } + + if was_unsatisfied { + debug!("@fromContext selection set is unsatisfied"); + return Ok(ConditionResolution::Unsatisfied { reason: None }); + } + + // it's possible that we will need to create a new fetch group at this point, in which + // case we'll need to collect the keys to jump back to this object as a precondition + // for satisfying it. + let (edge_head, _) = self.graph.edge_endpoints(edge)?; + if self.graph.get_locally_satisfiable_key(edge_head)?.is_none() { + debug!("Post-context conditions cannot be satisfied"); + return Ok(ConditionResolution::Unsatisfied { + reason: Some(UnsatisfiedConditionReason::NoPostRequireKey), + }); + } + + if edge_weight.conditions.is_none() { + return Ok(ConditionResolution::Satisfied { + cost: total_cost, + path_tree: None, + context_map: Some(context_map), + }); + } + } + + /* Resolve all other conditions */ + debug_span!("Checking conditions {conditions} on edge {edge_weight}"); - let resolution = condition_resolver.resolve( + let mut resolution = condition_resolver.resolve( edge, context, excluded_destinations, excluded_conditions, + None, )?; if let Some(Some(last_edge)) = self.edges.last().map(|e| (*e).into()) { if matches!( @@ -1424,6 +1915,13 @@ where } } } + if let ConditionResolution::Satisfied { + context_map: ctx_map, + .. + } = &mut resolution + { + *ctx_map = Some(context_map); + } debug!("Condition resolution: {resolution:?}"); Ok(resolution) } @@ -1593,7 +2091,12 @@ where &excluded_destinations.add_excluded(&edge_tail_weight.source), excluded_conditions, )?; - if let ConditionResolution::Satisfied { path_tree, cost } = condition_resolution { + if let ConditionResolution::Satisfied { + path_tree, + cost, + context_map, + } = condition_resolution + { debug!("Condition satisfied"); drop(guard); // We can get to `edge_tail_weight.source` with that edge. But if we had already @@ -1780,7 +2283,11 @@ where let updated_path = Arc::new(to_advance.add( transition_and_context_to_trigger(&edge_weight.transition, context), edge.into(), - ConditionResolution::Satisfied { cost, path_tree }, + ConditionResolution::Satisfied { + cost, + path_tree, + context_map, + }, None, )?); best_path_by_source.insert( @@ -2179,6 +2686,8 @@ impl OpGraphPath { runtime_types_before_tail_if_last_is_cast: None, // TODO: The JS codebase copied this from the current path, which seems like a bug. defer_on_tail: self.defer_on_tail.clone(), + context_to_selection: self.context_to_selection.clone(), + parameter_to_context: self.parameter_to_context.clone(), }) } @@ -2503,14 +3012,14 @@ impl OpGraphPath { operation_field, edge_weight, self, ))); } - operation_field = Field::new(FieldData { + operation_field = Field { schema: self.graph.schema_by_source(&tail_weight.source)?.clone(), field_position: field_on_tail_type.into(), alias: operation_field.alias.clone(), arguments: operation_field.arguments.clone(), directives: operation_field.directives.clone(), sibling_typename: operation_field.sibling_typename.clone(), - }) + } } let field_path = self.add_field_edge( @@ -2696,19 +3205,15 @@ impl OpGraphPath { debug!("Handling implementation {implementation_type_pos}"); let span = debug_span!(" |"); let guard = span.enter(); - let implementation_inline_fragment = - InlineFragment::new(InlineFragmentData { - schema: self - .graph - .schema_by_source(&tail_weight.source)? - .clone(), - parent_type_position: tail_type_pos.clone().into(), - type_condition_position: Some( - implementation_type_pos.clone().into(), - ), - directives: Default::default(), - selection_id: SelectionId::new(), - }); + let implementation_inline_fragment = InlineFragment { + schema: self.graph.schema_by_source(&tail_weight.source)?.clone(), + parent_type_position: tail_type_pos.clone().into(), + type_condition_position: Some( + implementation_type_pos.clone().into(), + ), + directives: Default::default(), + selection_id: SelectionId::new(), + }; let implementation_options = SimultaneousPathsWithLazyIndirectPaths::new( self.clone().into(), @@ -2903,19 +3408,15 @@ impl OpGraphPath { for implementation_type_pos in intersection { let span = debug_span!("Trying {implementation_type_pos}"); let guard = span.enter(); - let implementation_inline_fragment = - InlineFragment::new(InlineFragmentData { - schema: self - .graph - .schema_by_source(&tail_weight.source)? - .clone(), - parent_type_position: tail_type_pos.clone().into(), - type_condition_position: Some( - implementation_type_pos.clone().into(), - ), - directives: operation_inline_fragment.directives.clone(), - selection_id: SelectionId::new(), - }); + let implementation_inline_fragment = InlineFragment { + schema: self.graph.schema_by_source(&tail_weight.source)?.clone(), + parent_type_position: tail_type_pos.clone().into(), + type_condition_position: Some( + implementation_type_pos.clone().into(), + ), + directives: operation_inline_fragment.directives.clone(), + selection_id: SelectionId::new(), + }; let implementation_options = SimultaneousPathsWithLazyIndirectPaths::new( self.clone().into(), @@ -2989,17 +3490,16 @@ impl OpGraphPath { if operation_inline_fragment.directives.is_empty() { return Ok((Some(vec![self.clone().into()]), None)); } - let operation_inline_fragment = - InlineFragment::new(InlineFragmentData { - schema: self - .graph - .schema_by_source(&tail_weight.source)? - .clone(), - parent_type_position: tail_type_pos.clone().into(), - type_condition_position: None, - directives: operation_inline_fragment.directives.clone(), - selection_id: SelectionId::new(), - }); + let operation_inline_fragment = InlineFragment { + schema: self + .graph + .schema_by_source(&tail_weight.source)? + .clone(), + parent_type_position: tail_type_pos.clone().into(), + type_condition_position: None, + directives: operation_inline_fragment.directives.clone(), + selection_id: SelectionId::new(), + }; let defer_directive_arguments = operation_inline_fragment.defer_directive_arguments()?; let fragment_path = self.add( @@ -3892,7 +4392,6 @@ mod tests { use petgraph::stable_graph::NodeIndex; use crate::operation::Field; - use crate::operation::FieldData; use crate::query_graph::build_query_graph::build_query_graph; use crate::query_graph::condition_resolver::ConditionResolution; use crate::query_graph::graph_path::OpGraphPath; @@ -3928,8 +4427,8 @@ mod tests { type_name: Name::new("T").unwrap(), field_name: Name::new("t").unwrap(), }; - let data = FieldData::from_position(&schema, pos.into()); - let trigger = OpGraphPathTrigger::OpPathElement(OpPathElement::Field(Field::new(data))); + let field = Field::from_position(&schema, pos.into()); + let trigger = OpGraphPathTrigger::OpPathElement(OpPathElement::Field(field)); let path = path .add( trigger, @@ -3937,6 +4436,7 @@ mod tests { ConditionResolution::Satisfied { cost: 0.0, path_tree: None, + context_map: None, }, None, ) @@ -3946,8 +4446,8 @@ mod tests { type_name: Name::new("ID").unwrap(), field_name: Name::new("id").unwrap(), }; - let data = FieldData::from_position(&schema, pos.into()); - let trigger = OpGraphPathTrigger::OpPathElement(OpPathElement::Field(Field::new(data))); + let field = Field::from_position(&schema, pos.into()); + let trigger = OpGraphPathTrigger::OpPathElement(OpPathElement::Field(field)); let path = path .add( trigger, @@ -3955,6 +4455,7 @@ mod tests { ConditionResolution::Satisfied { cost: 0.0, path_tree: None, + context_map: None, }, None, ) diff --git a/apollo-federation/src/query_graph/mod.rs b/apollo-federation/src/query_graph/mod.rs index 344d9dc01d..cb21e80a95 100644 --- a/apollo-federation/src/query_graph/mod.rs +++ b/apollo-federation/src/query_graph/mod.rs @@ -6,7 +6,9 @@ use std::sync::Arc; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::schema::NamedType; +use apollo_compiler::schema::Type; use apollo_compiler::Name; +use apollo_compiler::Node; use petgraph::graph::DiGraph; use petgraph::graph::EdgeIndex; use petgraph::graph::EdgeReference; @@ -16,6 +18,7 @@ use petgraph::Direction; use crate::error::FederationError; use crate::error::SingleFederationError; +use crate::internal_error; use crate::operation::Field; use crate::operation::InlineFragment; use crate::operation::SelectionSet; @@ -23,6 +26,7 @@ use crate::schema::field_set::parse_field_set; use crate::schema::position::CompositeTypeDefinitionPosition; use crate::schema::position::FieldDefinitionPosition; use crate::schema::position::InterfaceFieldDefinitionPosition; +use crate::schema::position::ObjectFieldArgumentDefinitionPosition; use crate::schema::position::ObjectTypeDefinitionPosition; use crate::schema::position::OutputTypeDefinitionPosition; use crate::schema::position::SchemaRootDefinitionKind; @@ -130,6 +134,20 @@ impl TryFrom for ObjectTypeDefinitionPosition { } } +/// Contains all of the data necessary to connect the object field (`coordinate`) with the +/// `@fromContext` to its (grand)parent types contain a matching selection. +#[derive(Debug, PartialEq, Clone)] +pub struct ContextCondition { + context: String, + subgraph_name: Arc, + selection: String, + types_with_context_set: IndexSet, + // PORT_NOTE: This field was renamed because the JS name (`coordinate`) was too vague. + argument_coordinate: ObjectFieldArgumentDefinitionPosition, + named_parameter: Name, + arg_type: Node, +} + #[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 @@ -154,9 +172,24 @@ pub(crate) struct QueryGraphEdge { /// one of them has an @override with a label. If the override condition /// matches the query plan parameters, this edge can be taken. pub(crate) override_condition: Option, + /// All fields with `@fromContext` that need access to parental type with corresponding + /// `@context`. + pub(crate) required_contexts: Vec, } impl QueryGraphEdge { + pub(crate) fn new( + transition: QueryGraphEdgeTransition, + conditions: Option>, + ) -> Self { + Self { + transition, + conditions, + override_condition: None, + required_contexts: Vec::new(), + } + } + fn satisfies_override_conditions( &self, conditions_to_check: &EnabledOverrideConditions, @@ -356,6 +389,11 @@ pub struct QueryGraph { /// lowered composition validation on a big composition (100+ subgraphs) from ~4 minutes to /// ~10 seconds. non_trivial_followup_edges: IndexMap>, + /// Maps each subgraph name to each field argument of `@fromContext`. + pub(crate) subgraph_to_args: IndexMap, Vec>, + /// Like `self.subgraph_to_args` but pairs each field argument with a unique identifier string. + pub(crate) subgraph_to_arg_indices: + IndexMap, IndexMap>, } impl QueryGraph { @@ -368,39 +406,27 @@ impl QueryGraph { } pub(crate) fn node_weight(&self, node: NodeIndex) -> Result<&QueryGraphNode, FederationError> { - self.graph.node_weight(node).ok_or_else(|| { - SingleFederationError::Internal { - message: "Node unexpectedly missing".to_owned(), - } - .into() - }) + self.graph + .node_weight(node) + .ok_or_else(|| internal_error!("Node unexpectedly missing")) } fn node_weight_mut(&mut self, node: NodeIndex) -> Result<&mut QueryGraphNode, FederationError> { - self.graph.node_weight_mut(node).ok_or_else(|| { - SingleFederationError::Internal { - message: "Node unexpectedly missing".to_owned(), - } - .into() - }) + self.graph + .node_weight_mut(node) + .ok_or_else(|| internal_error!("Node unexpectedly missing")) } pub(crate) fn edge_weight(&self, edge: EdgeIndex) -> Result<&QueryGraphEdge, FederationError> { - self.graph.edge_weight(edge).ok_or_else(|| { - SingleFederationError::Internal { - message: "Edge unexpectedly missing".to_owned(), - } - .into() - }) + self.graph + .edge_weight(edge) + .ok_or_else(|| internal_error!("Edge unexpectedly missing")) } fn edge_weight_mut(&mut self, edge: EdgeIndex) -> Result<&mut QueryGraphEdge, FederationError> { - self.graph.edge_weight_mut(edge).ok_or_else(|| { - SingleFederationError::Internal { - message: "Edge unexpectedly missing".to_owned(), - } - .into() - }) + self.graph + .edge_weight_mut(edge) + .ok_or_else(|| internal_error!("Edge unexpectedly missing")) } pub(crate) fn edge_head_weight( @@ -415,12 +441,9 @@ impl QueryGraph { &self, edge: EdgeIndex, ) -> Result<(NodeIndex, NodeIndex), FederationError> { - self.graph.edge_endpoints(edge).ok_or_else(|| { - SingleFederationError::Internal { - message: "Edge unexpectedly missing".to_owned(), - } - .into() - }) + self.graph + .edge_endpoints(edge) + .ok_or_else(|| internal_error!("Edge unexpectedly missing")) } fn schema(&self) -> Result<&ValidFederationSchema, FederationError> { @@ -431,12 +454,9 @@ impl QueryGraph { &self, source: &str, ) -> Result<&ValidFederationSchema, FederationError> { - self.sources.get(source).ok_or_else(|| { - SingleFederationError::Internal { - message: "Schema unexpectedly missing".to_owned(), - } - .into() - }) + self.sources + .get(source) + .ok_or_else(|| internal_error!(r#"Schema for "{source}" unexpectedly missing"#)) } pub(crate) fn subgraph_schemas(&self) -> &IndexMap, ValidFederationSchema> { @@ -454,7 +474,7 @@ impl QueryGraph { ) -> Result<&IndexSet, FederationError> { self.types_to_nodes()? .get(name) - .ok_or_else(|| FederationError::internal("No nodes unexpectedly found for type")) + .ok_or_else(|| internal_error!("No nodes unexpectedly found for type")) } pub(crate) fn types_to_nodes( @@ -616,6 +636,7 @@ impl QueryGraph { &OpGraphPathContext::default(), &ExcludedDestinations::default(), &ExcludedConditions::default(), + None, )?; let ConditionResolution::Satisfied { cost, .. } = condition_resolution else { continue; diff --git a/apollo-federation/src/query_graph/path_tree.rs b/apollo-federation/src/query_graph/path_tree.rs index cb6b9fef18..b4f4e820cb 100644 --- a/apollo-federation/src/query_graph/path_tree.rs +++ b/apollo-federation/src/query_graph/path_tree.rs @@ -9,6 +9,8 @@ use petgraph::graph::EdgeIndex; use petgraph::graph::NodeIndex; use serde::Serialize; +use super::graph_path::ContextToSelection; +use super::graph_path::ParameterToContext; use crate::error::FederationError; use crate::operation::SelectionSet; use crate::query_graph::graph_path::GraphPathItem; @@ -92,6 +94,10 @@ where pub(crate) conditions: Option>, /// The child `PathTree` reached by taking the edge. pub(crate) tree: Arc>, + // a list of contexts set at this point in the path tree + pub(crate) context_to_selection: Option, + // a list of contexts used at this point in the path tree + pub(crate) parameter_to_context: Option, } impl PartialEq for PathTreeChild @@ -242,12 +248,21 @@ where trigger: &'inputs Arc, conditions: Option>, sub_paths_and_selections: Vec<(GraphPathIter, Option<&'inputs Arc>)>, + context_to_selection: Option, + parameter_to_context: Option, } let mut local_selection_sets = Vec::new(); for (mut graph_path_iter, selection) in graph_paths_and_selections { - let Some((generic_edge, trigger, conditions)) = graph_path_iter.next() else { + let Some(( + generic_edge, + trigger, + conditions, + context_to_selection, + parameter_to_context, + )) = graph_path_iter.next() + else { // End of an input `GraphPath` if let Some(selection) = selection { local_selection_sets.push(selection.clone()); @@ -274,6 +289,18 @@ where let existing = entry.into_mut(); existing.trigger = trigger; existing.conditions = merge_conditions(&existing.conditions, conditions); + if let Some(other) = context_to_selection { + existing + .context_to_selection + .get_or_insert_with(Default::default) + .extend(other); + } + if let Some(other) = parameter_to_context { + existing + .parameter_to_context + .get_or_insert_with(IndexMap::default) + .extend(other); + } existing .sub_paths_and_selections .push((graph_path_iter, selection)) @@ -284,6 +311,8 @@ where trigger, conditions: conditions.clone(), sub_paths_and_selections: vec![(graph_path_iter, selection)], + context_to_selection, + parameter_to_context, }); } } @@ -301,6 +330,8 @@ where by_unique_edge.target_node, child.sub_paths_and_selections, )?), + context_to_selection: child.context_to_selection.clone(), + parameter_to_context: child.parameter_to_context.clone(), })) } } @@ -334,6 +365,8 @@ where (Some(cond_a), Some(cond_b)) => cond_a.equals_same_root(cond_b), _ => false, } + && a.context_to_selection == b.context_to_selection + && a.parameter_to_context == b.parameter_to_context && a.tree.equals_same_root(&b.tree) }) } @@ -415,6 +448,14 @@ where trigger: child.trigger.clone(), conditions: merge_conditions(&child.conditions, &other_child.conditions), tree: child.tree.merge(&other_child.tree), + context_to_selection: merge_context_to_selection( + &child.context_to_selection, + &other_child.context_to_selection, + ), + parameter_to_context: merge_parameter_to_context( + &child.parameter_to_context, + &other_child.parameter_to_context, + ), }) } else { childs.push(other_child.clone()) @@ -436,6 +477,40 @@ where } } +fn merge_context_to_selection( + a: &Option, + b: &Option, +) -> Option { + match (a, b) { + (Some(a), Some(b)) => { + let mut merged: ContextToSelection = Default::default(); + merged.extend(a.iter().cloned()); + merged.extend(b.iter().cloned()); + Some(merged) + } + (Some(a), None) => Some(a.clone()), + (None, Some(b)) => Some(b.clone()), + (None, None) => None, + } +} + +fn merge_parameter_to_context( + a: &Option, + b: &Option, +) -> Option { + match (a, b) { + (Some(a), Some(b)) => { + let mut merged: ParameterToContext = Default::default(); + merged.extend(a.iter().map(|(k, v)| (k.clone(), v.clone()))); + merged.extend(b.iter().map(|(k, v)| (k.clone(), v.clone()))); + Some(merged) + } + (Some(a), None) => Some(a.clone()), + (None, Some(b)) => Some(b.clone()), + (None, None) => None, + } +} + fn merge_conditions( a: &Option>, b: &Option>, @@ -480,7 +555,6 @@ mod tests { use crate::error::FederationError; use crate::operation::normalize_operation; use crate::operation::Field; - use crate::operation::FieldData; use crate::query_graph::build_query_graph::build_query_graph; use crate::query_graph::condition_resolver::ConditionResolution; use crate::query_graph::graph_path::OpGraphPath; @@ -508,6 +582,7 @@ mod tests { ConditionResolution::Satisfied { cost: 0.0, path_tree: None, + context_map: None, } } @@ -546,7 +621,7 @@ mod tests { .unwrap(); // build the trigger for the edge - let data = FieldData { + let field = Field { schema: query_graph.schema().unwrap().clone(), field_position: field_def.clone(), alias: None, @@ -554,7 +629,7 @@ mod tests { directives: Default::default(), sibling_typename: None, }; - let trigger = OpGraphPathTrigger::OpPathElement(OpPathElement::Field(Field::new(data))); + let trigger = OpGraphPathTrigger::OpPathElement(OpPathElement::Field(field)); // add the edge to the path graph_path = graph_path diff --git a/apollo-federation/src/query_plan/conditions.rs b/apollo-federation/src/query_plan/conditions.rs index cf248c8b61..f202fd4058 100644 --- a/apollo-federation/src/query_plan/conditions.rs +++ b/apollo-federation/src/query_plan/conditions.rs @@ -8,8 +8,8 @@ use apollo_compiler::Node; use indexmap::map::Entry; use serde::Serialize; +use crate::bail; use crate::error::FederationError; -use crate::internal_error; use crate::operation::DirectiveList; use crate::operation::NamedFragments; use crate::operation::Selection; @@ -122,7 +122,7 @@ impl Conditions { if let Some(skip) = directives.get("skip") { let Some(value) = skip.specified_argument_by_name("if") else { - internal_error!("missing @skip(if:) argument"); + bail!("missing @skip(if:) argument"); }; match value.as_ref() { @@ -134,14 +134,14 @@ impl Conditions { variables.insert(name.clone(), ConditionKind::Skip); } _ => { - internal_error!("expected boolean or variable `if` argument, got {value}"); + bail!("expected boolean or variable `if` argument, got {value}"); } } } if let Some(include) = directives.get("include") { let Some(value) = include.specified_argument_by_name("if") else { - internal_error!("missing @include(if:) argument"); + bail!("missing @include(if:) argument"); }; match value.as_ref() { @@ -159,7 +159,7 @@ impl Conditions { } } _ => { - internal_error!("expected boolean or variable `if` argument, got {value}"); + bail!("expected boolean or variable `if` argument, got {value}"); } } } diff --git a/apollo-federation/src/query_plan/display.rs b/apollo-federation/src/query_plan/display.rs index b6416590e1..48e9168684 100644 --- a/apollo-federation/src/query_plan/display.rs +++ b/apollo-federation/src/query_plan/display.rs @@ -377,6 +377,7 @@ impl fmt::Display for FetchDataPathElement { write_conditions(conditions, f) } Self::TypenameEquals(name) => write!(f, "... on {name}"), + Self::Parent => write!(f, ".."), } } } diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index f31bd0d2ff..4a2da90e95 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -9,7 +9,6 @@ use apollo_compiler::ast::Argument; use apollo_compiler::ast::Directive; use apollo_compiler::ast::OperationType; use apollo_compiler::ast::Type; -use apollo_compiler::collections::HashMap; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::executable; @@ -28,6 +27,8 @@ use petgraph::visit::IntoNodeReferences; use serde::Serialize; use super::query_planner::SubgraphOperationCompression; +use super::FetchDataKeyRenamer; +use crate::bail; use crate::display_helpers::DisplayOption; use crate::error::FederationError; use crate::error::SingleFederationError; @@ -36,9 +37,7 @@ use crate::operation::ArgumentList; use crate::operation::ContainmentOptions; use crate::operation::DirectiveList; use crate::operation::Field; -use crate::operation::FieldData; use crate::operation::InlineFragment; -use crate::operation::InlineFragmentData; use crate::operation::InlineFragmentSelection; use crate::operation::Operation; use crate::operation::Selection; @@ -88,7 +87,7 @@ type DeferRef = String; /// Like a multimap with a Set instead of a Vec for value storage. #[derive(Debug, Clone, Default)] struct DeferredNodes { - inner: HashMap>>, + inner: IndexMap>>, } impl DeferredNodes { fn new() -> Self { @@ -169,6 +168,8 @@ pub(crate) struct FetchDependencyGraphNode { inputs: Option>, /// Input rewrites for query plan execution to perform prior to executing the fetch. input_rewrites: Arc>>, + /// Rewrites that will need to occur to store contextual data for future use + context_inputs: Vec, /// As query plan execution runs, it accumulates fetch data into a response object. This is the /// path at which to merge in the data for this particular fetch. merge_at: Option>, @@ -227,6 +228,8 @@ pub(crate) struct FetchInputs { /// The supergraph schema (primarily used for validation of added selection sets). #[serde(skip)] supergraph_schema: ValidFederationSchema, + /// Contexts used as inputs + used_contexts: IndexMap>, } /// Represents a dependency between two subgraph fetches, namely that the tail/child depends on the @@ -587,7 +590,6 @@ impl FetchDependencyGraphNodePath { fn advance_field_type(&self, element: &Field) -> Result, FederationError> { if !element - .data() .output_base_type() .map(|base_type| base_type.is_composite_type()) .unwrap_or_default() @@ -660,7 +662,7 @@ impl FetchDependencyGraphNodePath { } new_path.push(FetchDataPathElement::Key( - field.response_name(), + field.response_name().clone(), Default::default(), )); @@ -794,6 +796,7 @@ impl FetchDependencyGraph { cached_cost: None, must_preserve_selection_set: false, is_known_useful: false, + context_inputs: Vec::new(), }))) } @@ -2346,11 +2349,11 @@ impl FetchDependencyGraph { &parent.selection_set.selection_set.schema, parent_op_path, )?; - let new_node_is_unneeded = node + let node_is_unneeded = node .selection_set .selection_set .can_rebase_on(&type_at_path, &parent.selection_set.selection_set.schema)?; - Ok(new_node_is_unneeded) + Ok(node_is_unneeded) } fn type_at_path( @@ -2513,6 +2516,14 @@ impl FetchDependencyGraphNode { Ok(()) } + fn add_input_context(&mut self, context: Name, ty: Node) -> Result<(), FederationError> { + let Some(inputs) = &mut self.inputs else { + bail!("Shouldn't try to add inputs to a root fetch node") + }; + Arc::make_mut(inputs).add_context(context, ty); + Ok(()) + } + fn copy_inputs(&mut self, other: &FetchDependencyGraphNode) -> Result<(), FederationError> { if let Some(other_inputs) = other.inputs.clone() { let inputs = self.inputs.get_or_insert_with(|| { @@ -2525,6 +2536,10 @@ impl FetchDependencyGraphNode { for rewrite in other.input_rewrites.iter() { input_rewrites.push(rewrite.clone()); } + + for context_input in &other.context_inputs { + self.add_context_renamer(context_input.clone()); + } } Ok(()) } @@ -2584,14 +2599,29 @@ impl FetchDependencyGraphNode { if self.selection_set.selection_set.selections.is_empty() { return Ok(None); } + let context_variable_definitions = self.inputs.iter().flat_map(|inputs| { + inputs.used_contexts.iter().map(|(context, ty)| { + Node::new(VariableDefinition { + name: context.clone(), + ty: ty.clone(), + default_value: None, + directives: Default::default(), + }) + }) + }); + let variable_definitions = variable_definitions + .iter() + .cloned() + .chain(context_variable_definitions) + .collect::>(); let (selection, output_rewrites) = - self.finalize_selection(variable_definitions, handled_conditions)?; + self.finalize_selection(&variable_definitions, handled_conditions)?; let input_nodes = self .inputs .as_ref() .map(|inputs| { inputs.to_selection_set_nodes( - variable_definitions, + &variable_definitions, handled_conditions, &self.parent_type, ) @@ -2697,7 +2727,12 @@ impl FetchDependencyGraphNode { operation_kind: self.root_kind.into(), input_rewrites: self.input_rewrites.clone(), output_rewrites, - context_rewrites: Default::default(), + context_rewrites: self + .context_inputs + .iter() + .cloned() + .map(|r| Arc::new(r.into())) + .collect(), })); Ok(Some(if let Some(path) = self.merge_at.clone() { @@ -2912,6 +2947,86 @@ impl FetchDependencyGraphNode { }; Some(format!("{subgraph_name}-{merge_at_str}")) } + + fn add_context_renamer(&mut self, renamer: FetchDataKeyRenamer) { + if !self.context_inputs.iter().any(|c| *c == renamer) { + self.context_inputs.push(renamer); + } + } + + fn add_context_renamers_for_selection_set( + &mut self, + selection_set: Option<&SelectionSet>, + relative_path: Vec, + alias: Name, + ) -> Result<(), FederationError> { + let selection_set = match selection_set { + Some(selection_set) if !selection_set.is_empty() => selection_set, + _ => { + self.add_context_renamer(FetchDataKeyRenamer { + path: relative_path, + rename_key_to: alias, + }); + return Ok(()); + } + }; + + for selection in selection_set { + match selection { + Selection::Field(field_selection) => { + if matches!(relative_path.last(), Some(FetchDataPathElement::Parent)) + && selection_set.type_position.type_name() != "Query" + { + for possible_runtime_type in selection_set + .schema + .possible_runtime_types(selection_set.type_position.clone())? + { + let mut new_relative_path = relative_path.clone(); + new_relative_path.push(FetchDataPathElement::TypenameEquals( + possible_runtime_type.type_name.clone(), + )); + self.add_context_renamers_for_selection_set( + Some(selection_set), + new_relative_path, + alias.clone(), + )?; + } + } else { + let mut new_relative_path = relative_path.clone(); + new_relative_path.push(FetchDataPathElement::Key( + field_selection.field.field_position.field_name().clone(), + Default::default(), + )); + self.add_context_renamers_for_selection_set( + field_selection.selection_set.as_ref(), + new_relative_path, + alias.clone(), + )?; + } + } + Selection::FragmentSpread(_) => { + bail!("Contexts shouldn't contain named fragment spreads"); + } + Selection::InlineFragment(inline_fragment_selection) => { + if let Some(type_condition) = &inline_fragment_selection + .inline_fragment + .type_condition_position + { + let mut new_relative_path = relative_path.clone(); + new_relative_path.push(FetchDataPathElement::TypenameEquals( + type_condition.type_name().clone(), + )); + self.add_context_renamers_for_selection_set( + Some(&inline_fragment_selection.selection_set), + new_relative_path, + alias.clone(), + )?; + } + } + } + } + Ok(()) + } } fn operation_for_entities_fetch( @@ -2952,7 +3067,7 @@ fn operation_for_entities_fetch( let entities = FieldDefinitionPosition::Object(query_type.field(ENTITIES_QUERY.clone())); let entities_call = Selection::from_element( - OpPathElement::Field(Field::new(FieldData { + OpPathElement::Field(Field { schema: subgraph_schema.clone(), field_position: entities, alias: None, @@ -2962,7 +3077,7 @@ fn operation_for_entities_fetch( )), directives: Default::default(), sibling_typename: None, - })), + }), Some(selection_set), )?; @@ -3096,6 +3211,7 @@ impl FetchInputs { Self { selection_sets_per_parent_type: Default::default(), supergraph_schema, + used_contexts: Default::default(), } } @@ -3121,7 +3237,12 @@ impl FetchInputs { other .selection_sets_per_parent_type .values() - .try_for_each(|selections| self.add(selections)) + .try_for_each(|selections| self.add(selections))?; + other + .used_contexts + .iter() + .for_each(|(context, ty)| self.add_context(context.clone(), ty.clone())); + Ok(()) } fn contains(&self, other: &Self) -> bool { @@ -3133,7 +3254,13 @@ impl FetchInputs { return false; } } - true + if self.used_contexts.len() < other.used_contexts.len() { + return false; + } + other + .used_contexts + .keys() + .all(|context| self.used_contexts.contains_key(context)) } fn equals(&self, other: &Self) -> bool { @@ -3156,8 +3283,13 @@ impl FetchInputs { } // so far so good } - // all clear - true + if self.used_contexts.len() != other.used_contexts.len() { + return false; + } + other + .used_contexts + .keys() + .all(|context| self.used_contexts.contains_key(context)) } fn to_selection_set_nodes( @@ -3180,6 +3312,10 @@ impl FetchInputs { selections: Arc::new(selections), }) } + + fn add_context(&mut self, context: Name, ty: Node) { + self.used_contexts.insert(context, ty); + } } impl std::fmt::Display for FetchInputs { @@ -3338,6 +3474,7 @@ struct ComputeNodesStackItem<'a> { node_path: FetchDependencyGraphNodePath, context: &'a OpGraphPathContext, defer_context: DeferContext, + context_to_condition_nodes: Arc>>, } #[cfg_attr( @@ -3352,17 +3489,14 @@ pub(crate) fn compute_nodes_for_tree( initial_defer_context: DeferContext, initial_conditions: &OpGraphPathContext, ) -> Result, FederationError> { - snapshot!( - "OpPathTree", - serde_json_bytes::json!(initial_tree.to_string()).to_string(), - "path_tree" - ); + snapshot!("OpPathTree", initial_tree.to_string(), "path_tree"); let mut stack = vec![ComputeNodesStackItem { tree: initial_tree, node_id: initial_node_id, node_path: initial_node_path, context: initial_conditions, defer_context: initial_defer_context, + context_to_condition_nodes: Arc::new(Default::default()), }]; let mut created_nodes = IndexSet::default(); while let Some(stack_item) = stack.pop() { @@ -3443,7 +3577,11 @@ pub(crate) fn compute_nodes_for_tree( } } } - snapshot!(dependency_graph, "updated_dependency_graph"); + snapshot!( + "FetchDependencyGraph", + dependency_graph.to_dot(), + "Fetch dependency graph updated by compute_nodes_for_tree" + ); Ok(created_nodes) } @@ -3598,6 +3736,7 @@ fn compute_nodes_for_key_resolution<'a>( )?), context: new_context, defer_context: updated_defer_context, + context_to_condition_nodes: stack_item.context_to_condition_nodes.clone(), }) } @@ -3699,6 +3838,7 @@ fn compute_nodes_for_root_type_resolution<'a>( context: new_context, defer_context: updated_defer_context, + context_to_condition_nodes: stack_item.context_to_condition_nodes.clone(), }) } @@ -3738,11 +3878,13 @@ fn compute_nodes_for_op_path_element<'a>( }, context: stack_item.context, defer_context: updated_defer_context, + context_to_condition_nodes: stack_item.context_to_condition_nodes.clone(), }); }; let (source_id, dest_id) = stack_item.tree.graph.edge_endpoints(edge_id)?; let source = stack_item.tree.graph.node_weight(source_id)?; let dest = stack_item.tree.graph.node_weight(dest_id)?; + let edge = stack_item.tree.graph.edge_weight(edge_id)?; if source.source != dest.source { return Err(FederationError::internal(format!( "Collecting edge {edge_id:?} for {operation_element:?} \ @@ -3796,21 +3938,227 @@ fn compute_nodes_for_op_path_element<'a>( node_path: stack_item.node_path.clone(), context: stack_item.context, defer_context: updated_defer_context, + context_to_condition_nodes: stack_item.context_to_condition_nodes.clone(), }; if let Some(conditions) = &child.conditions { // We have @requires or some other dependency to create nodes for. - let (required_node_id, require_path) = handle_requires( + let conditions_node_data = handle_conditions_tree( dependency_graph, - edge_id, conditions, (stack_item.node_id, &stack_item.node_path), - stack_item.context, + // If setting a context, add __typename to the site where we are retrieving context from + // since the context rewrites path will start with a type condition. + if child.context_to_selection.is_some() { + Some(edge_id) + } else { + None + }, &updated.defer_context, created_nodes, )?; - updated.node_id = required_node_id; - updated.node_path = require_path; + + if let Some(context_to_selection) = &child.context_to_selection { + let mut condition_nodes = vec![conditions_node_data.conditions_merge_node_id]; + condition_nodes.extend(&conditions_node_data.created_node_ids); + let mut context_to_condition_nodes = + stack_item.context_to_condition_nodes.deref().clone(); + for context in context_to_selection { + context_to_condition_nodes.insert(context.clone(), condition_nodes.clone()); + } + updated.context_to_condition_nodes = Arc::new(context_to_condition_nodes); + } + + if edge.conditions.is_some() { + // This edge needs the conditions just fetched, to be provided via _entities (@requires + // or fake interface object downcast). So we create the post-@requires group, adding the + // subgraph jump (if it isn't optimized away). + let (required_node_id, require_path) = create_post_requires_node( + dependency_graph, + edge_id, + (stack_item.node_id, &stack_item.node_path), + stack_item.context, + conditions_node_data, + created_nodes, + )?; + updated.node_id = required_node_id; + updated.node_path = require_path; + } } + + // If the edge uses context variables, every context used must be set in a different parent + // node or else we need to create a new one. + if let Some(parameter_to_context) = &child.parameter_to_context { + let mut conditions_nodes: IndexSet = Default::default(); + let mut is_subgraph_jump_needed = false; + for context_entry in parameter_to_context.values() { + let Some(context_nodes) = updated + .context_to_condition_nodes + .get(&context_entry.context_id) + else { + bail!( + "Could not find condition nodes for context {}", + context_entry.context_id + ); + }; + conditions_nodes.extend(context_nodes); + if context_nodes + .first() + .is_some_and(|node_id| *node_id == updated.node_id) + { + is_subgraph_jump_needed = true; + } + } + if is_subgraph_jump_needed { + if updated.node_id != stack_item.node_id { + bail!("Node created by post-@requires handling shouldn't have set context already"); + } + + let source_type: CompositeTypeDefinitionPosition = source.type_.clone().try_into()?; + let source_schema: ValidFederationSchema = dependency_graph + .federated_query_graph + .schema_by_source(&source.source)? + .clone(); + let path_in_parent = &stack_item.node_path.path_in_node; + // NOTE: We should re-examine defer-handling for path elements in this function in the + // future to ensure they're working as intended. + let new_node_id = dependency_graph.get_or_create_key_node( + &source.source, + &stack_item.node_path.response_path, + &source_type, + ParentRelation { + parent_node_id: stack_item.node_id, + path_in_parent: Some(Arc::clone(path_in_parent)), + }, + &conditions_nodes, + None, + )?; + created_nodes.insert(new_node_id); + updated.node_id = new_node_id; + updated.node_path = stack_item + .node_path + .for_new_key_fetch(create_fetch_initial_path( + &dependency_graph.supergraph_schema, + &source_type, + stack_item.context, + )?); + + let Some(key_condition) = stack_item + .tree + .graph + .get_locally_satisfiable_key(source_id)? + else { + bail!( + "can_satisfy_conditions() validation should have required a key to be present for edge {}", + edge, + ) + }; + let mut key_inputs = + SelectionSet::for_composite_type(source_schema.clone(), source_type.clone()); + key_inputs.add_selection_set(&key_condition)?; + let node = FetchDependencyGraph::node_weight_mut( + &mut dependency_graph.graph, + stack_item.node_id, + )?; + node.selection_set + .add_at_path(path_in_parent, Some(&Arc::new(key_inputs)))?; + + let Ok(input_type): Result = dependency_graph + .supergraph_schema + .get_type(source_type.type_name().clone())? + .try_into() + else { + bail!( + "Type {} should exist in the supergraph and be a composite type", + source_type.type_name() + ); + }; + let mut input_selection_set = SelectionSet::for_composite_type( + dependency_graph.supergraph_schema.clone(), + input_type.clone(), + ); + input_selection_set.add_selection_set(&key_condition)?; + let inputs = wrap_input_selections( + &dependency_graph.supergraph_schema, + &input_type, + input_selection_set, + stack_item.context, + ); + let input_rewrites = compute_input_rewrites_on_key_fetch( + source_type.type_name(), + &source_type, + &source_schema, + )?; + let updated_node = FetchDependencyGraph::node_weight_mut( + &mut dependency_graph.graph, + updated.node_id, + )?; + updated_node.add_inputs(&inputs, input_rewrites.into_iter().flatten())?; + + // Add the condition nodes as parent nodes. + for parent_node_id in conditions_nodes { + dependency_graph.add_parent( + updated.node_id, + ParentRelation { + parent_node_id, + path_in_parent: None, + }, + ); + } + + // Add context renamers. + for context_entry in parameter_to_context.values() { + let updated_node = FetchDependencyGraph::node_weight_mut( + &mut dependency_graph.graph, + updated.node_id, + )?; + updated_node.add_input_context( + context_entry.context_id.clone(), + context_entry.subgraph_arg_type.clone(), + )?; + updated_node.add_context_renamers_for_selection_set( + Some(&context_entry.selection_set), + context_entry.relative_path.clone(), + context_entry.context_id.clone(), + )?; + } + } else { + // In this case we can just continue with the current node, but we need to add the + // condition nodes as parents and the context renamers. + for parent_node_id in conditions_nodes { + dependency_graph.add_parent( + updated.node_id, + ParentRelation { + parent_node_id, + path_in_parent: None, + }, + ); + } + let num_fields = updated + .node_path + .path_in_node + .iter() + .filter(|e| matches!((**e).deref(), OpPathElement::Field(_))) + .count(); + for context_entry in parameter_to_context.values() { + let new_relative_path = &context_entry.relative_path + [..(context_entry.relative_path.len() - num_fields)]; + let updated_node = FetchDependencyGraph::node_weight_mut( + &mut dependency_graph.graph, + updated.node_id, + )?; + updated_node.add_input_context( + context_entry.context_id.clone(), + context_entry.subgraph_arg_type.clone(), + )?; + updated_node.add_context_renamers_for_selection_set( + Some(&context_entry.selection_set), + new_relative_path.to_vec(), + context_entry.context_id.clone(), + )?; + } + } + } + if let OpPathElement::Field(field) = &updated_operation { if *field.name() == TYPENAME_FIELD { // Because of the optimization done in `QueryPlanner.optimizeSiblingTypenames`, @@ -3853,7 +4201,6 @@ fn compute_nodes_for_op_path_element<'a>( updated_node.must_preserve_selection_set = true } } - let edge = child.tree.graph.edge_weight(edge_id)?; if let QueryGraphEdgeTransition::InterfaceObjectFakeDownCast { .. } = &edge.transition { // We shouldn't add the operation "as is" as it's a down-cast but we're "faking it". // However, if the operation has directives, we should preserve that. @@ -3897,13 +4244,13 @@ fn wrap_selection_with_type_and_conditions( // PORT_NOTE: JS code looks for type condition in the wrapping type's schema based on // the name of wrapping type. Not sure why. return wrap_in_fragment( - InlineFragment::new(InlineFragmentData { + InlineFragment { schema: supergraph_schema.clone(), parent_type_position: wrapping_type.clone(), type_condition_position: Some(type_condition.clone()), directives: Default::default(), // None selection_id: SelectionId::new(), - }), + }, initial, ); } @@ -3924,13 +4271,13 @@ fn wrap_selection_with_type_and_conditions( .into()], }; wrap_in_fragment( - InlineFragment::new(InlineFragmentData { + InlineFragment { schema: supergraph_schema.clone(), parent_type_position: wrapping_type.clone(), type_condition_position: Some(type_condition.clone()), directives: [directive].into_iter().collect(), selection_id: SelectionId::new(), - }), + }, acc, ) }) @@ -4067,134 +4414,183 @@ fn extract_defer_from_operation( Ok((updated_operation_element, updated_context)) } -fn handle_requires( +struct ConditionsNodeData { + conditions_merge_node_id: NodeIndex, + path_in_conditions_merge_node_id: Option>, + created_node_ids: Vec, + is_fully_local_requires: bool, +} + +/// Computes nodes for conditions imposed by @requires, @fromContext, and @interfaceObject, merging +/// them into ancestors as an optimization if possible. This does not modify the current node to +/// use the condition data as input, nor does it create parent-child relationships with created +/// nodes and the current node. +fn handle_conditions_tree( dependency_graph: &mut FetchDependencyGraph, - query_graph_edge_id: EdgeIndex, - requires_conditions: &OpPathTree, + conditions: &OpPathTree, (fetch_node_id, fetch_node_path): (NodeIndex, &FetchDependencyGraphNodePath), - context: &OpGraphPathContext, + query_graph_edge_id_if_typename_needed: Option, defer_context: &DeferContext, created_nodes: &mut IndexSet, -) -> Result<(NodeIndex, FetchDependencyGraphNodePath), FederationError> { - // @requires should be on an entity type, and we only support object types right now - let head = dependency_graph - .federated_query_graph - .edge_head_weight(query_graph_edge_id)?; - let entity_type_schema = dependency_graph - .federated_query_graph - .schema_by_source(&head.source)? - .clone(); - let QueryGraphNodeType::SchemaType(OutputTypeDefinitionPosition::Object(entity_type_position)) = - head.type_.clone() - else { - return Err(FederationError::internal( - "@requires applied on non-entity object type", - )); - }; - - // In many cases, we can optimize @requires by merging the requirement to previously existing nodes. However, - // we only do this when the current node has only a single parent (it's hard to reason about it otherwise). - // But the current node could have multiple parents due to the graph lacking minimality, and we don't want that - // to needlessly prevent us from this optimization. So we do a graph reduction first (which effectively - // just eliminate unnecessary edges). To illustrate, we could be in a case like: +) -> Result { + // In many cases, we can optimize conditions by merging the fields into previously existing + // nodes. However, we only do this when the current node has only a single parent (it's hard to + // reason about it otherwise). But the current node could have multiple parents due to the graph + // lacking minimality, and we don't want that to needlessly prevent us from this optimization. + // So we do a graph reduction first (which effectively just eliminates unnecessary edges). To + // illustrate, we could be in a case like: // 1 // / \ // 0 --- 2 - // with current node 2. And while the node currently has 2 parents, the `reduce` step will ensure - // the edge `0 --- 2` is removed (since the dependency of 2 on 0 is already provide transitively through 1). + // with current node 2. And while the node currently has 2 parents, the `reduce` step will + // ensure the edge `0 --- 2` is removed (since the dependency of 2 on 0 is already provided + // transitively through 1). dependency_graph.reduce(); - let single_parent = iter_into_single_item(dependency_graph.parents_relations_of(fetch_node_id)); - // In general, we should do like for an edge, and create a new node _for the current subgraph_ - // that depends on the created_nodes and have the created nodes depend on the current one. - // However, we can be more efficient in general (and this is expected by the user) because - // required fields will usually come just after a key edge (at the top of a fetch node). - // In that case (when the path is only type_casts), we can put the created nodes directly - // as dependency of the current node, avoiding creation of a new one. Additionally, if the + // In general, we should do like for a key edge, and create a new node _for the current + // subgraph_ that depends on the created nodes and have the created nodes depend on the current + // one. However, we can be more efficient in general (and this is expected by the user) because + // condition fields will usually come just after a key edge (at the top of a fetch node). + // In that case (when the path is only type conditions), we can put the created nodes directly + // as dependencies of the current node, avoiding creation of a new one. Additionally, if the // node we're coming from is our "direct parent", we can merge it to said direct parent (which - // effectively means that the parent node will collect the provides before taking the edge - // to our current node). - if single_parent.is_some() && fetch_node_path.path_in_node.has_only_fragments() { - // Should do `if let` but it requires extra indentation. - let parent = single_parent.unwrap(); - - // We start by computing the nodes for the conditions. We do this using a copy of the current - // node (with only the inputs) as that allows to modify this copy without modifying `node`. - let fetch_node = dependency_graph.node_weight(fetch_node_id)?; - let subgraph_name = fetch_node.subgraph_name.clone(); - let Some(merge_at) = fetch_node.merge_at.clone() else { - return Err(FederationError::internal(format!( - "Fetch node {} merge_at_path is required but was missing", - fetch_node_id.index() - ))); + // effectively means that the parent node will collect subgraph-local condition fields before + // taking the edge to our current node). + let copied_node_id_and_parent = + match iter_into_single_item(dependency_graph.parents_relations_of(fetch_node_id)) { + Some(parent) if fetch_node_path.path_in_node.has_only_fragments() => { + // Since we may want the condition fields in this case to be added into an earlier + // node, we create a copy of the current node (with only the inputs), and the + // condition fields will be added to this node instead of the current one. + let fetch_node = dependency_graph.node_weight(fetch_node_id)?; + let subgraph_name = fetch_node.subgraph_name.clone(); + let Some(merge_at) = fetch_node.merge_at.clone() else { + bail!( + "Fetch node {} merge_at_path is required but was missing", + fetch_node_id.index() + ); + }; + let defer_ref = fetch_node.defer_ref.clone(); + let copied_node_id = + dependency_graph.new_key_node(&subgraph_name, merge_at, defer_ref.clone())?; + dependency_graph.add_parent(copied_node_id, parent.clone()); + dependency_graph.copy_inputs(copied_node_id, fetch_node_id)?; + Some((copied_node_id, parent)) + } + _ => None, }; - let defer_ref = fetch_node.defer_ref.clone(); - let new_node_id = - dependency_graph.new_key_node(&subgraph_name, merge_at, defer_ref.clone())?; - dependency_graph.add_parent(new_node_id, parent.clone()); - dependency_graph.copy_inputs(new_node_id, fetch_node_id)?; - let newly_created_node_ids = compute_nodes_for_tree( - dependency_graph, - requires_conditions, - new_node_id, - fetch_node_path.clone(), - defer_context.for_conditions(), - &OpGraphPathContext::default(), - )?; - if newly_created_node_ids.is_empty() { - // All conditions were local. Just merge the newly created node back into the current node (we didn't need it) - // and continue. - if !dependency_graph.can_merge_sibling_in(fetch_node_id, new_node_id)? { - return Err(FederationError::internal(format!( + let condition_node_id = match &copied_node_id_and_parent { + Some((copied_node_id, _)) => *copied_node_id, + None => fetch_node_id, + }; + if let Some(query_graph_edge_id) = query_graph_edge_id_if_typename_needed { + let head = dependency_graph + .federated_query_graph + .edge_head_weight(query_graph_edge_id)?; + let head_type: CompositeTypeDefinitionPosition = head.type_.clone().try_into()?; + let head_schema = dependency_graph + .federated_query_graph + .schema_by_source(&head.source)? + .clone(); + let typename_field = Arc::new(OpPathElement::Field(Field::new_introspection_typename( + &head_schema, + &head_type, + None, + ))); + let typename_path = fetch_node_path.path_in_node.with_pushed(typename_field); + let condition_node = + FetchDependencyGraph::node_weight_mut(&mut dependency_graph.graph, condition_node_id)?; + condition_node + .selection_set_mut() + .add_at_path(&typename_path, None)?; + } + + // Compute the node changes/additions introduced by the conditions path tree, using either the + // current node or the copy of the current node if we expect to optimize. + let newly_created_node_ids = compute_nodes_for_tree( + dependency_graph, + conditions, + condition_node_id, + fetch_node_path.clone(), + defer_context.for_conditions(), + &OpGraphPathContext::default(), + )?; + + if newly_created_node_ids.is_empty() { + // All conditions were local. If we copied the node expecting to optimize, just merge it + // back into the current node (we didn't need it) and continue. + // + // NOTE: This behavior is largely to maintain backwards compatibility with @requires. For + // @fromContext, it still may be useful to merge the conditions into the parent if possible. + // but we leave this optimization for later. + if let Some((copied_node_id, _)) = copied_node_id_and_parent { + if !dependency_graph.can_merge_sibling_in(fetch_node_id, copied_node_id)? { + bail!( "We should be able to merge {} into {} by construction", - new_node_id.index(), + copied_node_id.index(), fetch_node_id.index() - ))); + ); } - dependency_graph.merge_sibling_in(fetch_node_id, new_node_id)?; - return Ok((fetch_node_id, fetch_node_path.clone())); + dependency_graph.merge_sibling_in(fetch_node_id, copied_node_id)?; } + return Ok(ConditionsNodeData { + conditions_merge_node_id: fetch_node_id, + path_in_conditions_merge_node_id: Some(Arc::new(Default::default())), + created_node_ids: vec![], + is_fully_local_requires: true, + }); + } - // We know the @requires needs `newly_created_node_ids`. We do want to know however if any of the conditions was - // fetched from our `new_node`. If not, then this means that the `newly_created_node_ids` don't really depend on - // the current `node` and can be dependencies of the parent (or even merged into this parent). + if let Some((copied_node_id, parent)) = copied_node_id_and_parent { + // We know the conditions depend on at least one created node. We do want to know, however, + // if any of the condition fields was fetched from our copied node. If not, then this means + // that the created nodes don't really depend on the current node and can be dependencies + // of the parent (or even merged into the parent). // - // So we want to know if anything in `new_node` selection cannot be fetched directly from the parent. - // For that, we first remove any of `new_node` inputs from its selection: in most case, `new_node` - // will just contain the key needed to jump back to its parent, and those would usually be the same - // as the inputs. And since by definition we know `new_node`'s inputs are already fetched, we - // know they are not things that we need. Then, we check if what remains (often empty) can be - // directly fetched from the parent. If it can, then we can just merge `new_node` into that parent. - // Otherwise, we will have to "keep it". - // Note: it is to be sure this test is not polluted by other things in `node` that we created `new_node`. - dependency_graph.remove_inputs_from_selection(new_node_id)?; - - let new_node_is_not_needed = dependency_graph.is_node_unneeded(new_node_id, &parent)?; + // So we want to know if anything in the copied node's selections cannot be fetched directly + // from the parent. For that, we first remove any of the copied node's inputs from its + // selections: in most cases, the copied node will just contain the key needed to jump back + // to its parent, and those would usually be the same as its inputs. And since by definition + // we know copied node's inputs are already fetched, we know they are not things that we + // need. Then, we check if what remains (often empty) can be directly fetched from the + // parent. If it can, then we can just merge the copied node into that parent. Otherwise, we + // will have to "keep it". + // + // NOTE: We have explicitly copied the current node without its selections, so the current + // node's fields should not pollute this check on the copied node. + dependency_graph.remove_inputs_from_selection(copied_node_id)?; + let copied_node_is_unneeded = dependency_graph.is_node_unneeded(copied_node_id, &parent)?; let mut unmerged_node_ids: Vec = Vec::new(); - if new_node_is_not_needed { - // Up to this point, `new_node` had no parent, so let's first merge `new_node` to the parent, thus "rooting" - // its children to it. Note that we just checked that `new_node` selection was just its inputs, so - // we know that merging it to the parent is mostly a no-op from that POV, except maybe for requesting - // a few additional `__typename` we didn't before (due to the exclusion of `__typename` in the `new_node_is_unneeded` check) - dependency_graph.merge_child_in(parent.parent_node_id, new_node_id)?; - - // Now, all created groups are going to be descendant of `parentGroup`. But some of them may actually be - // mergeable into it. + if copied_node_is_unneeded { + // We've removed the copied node's inputs from its own selections, and confirmed the + // remaining fields can be fetched from the parent. As an optimization, we now merge it + // into the parent, thus "rooting" the copied node's children to that parent. Note that + // the copied node's selections are often empty after removing inputs, so merging it + // into the parent is usually a no-op from that POV, except maybe for requesting + // a few additional `__typename`s we didn't before. + dependency_graph.merge_child_in(parent.parent_node_id, copied_node_id)?; + + // Now, all created nodes are going to be descendants of the parent node. But some of + // them may actually be mergeable into it. for created_node_id in newly_created_node_ids { - // Note that `created_node_id` may not be a direct child of `parent_node_id`, but `can_merge_child_in` just return `false` in - // that case, yielding the behaviour we want (not trying to merge it in). + // Note that `created_node_id` may not be a direct child of `parent_node_id`, but + // `can_merge_child_in()` just returns `false` in that case, yielding the behavior + // we want (not trying to merge it in). if dependency_graph.can_merge_child_in(parent.parent_node_id, created_node_id)? { dependency_graph.merge_child_in(parent.parent_node_id, created_node_id)?; } else { unmerged_node_ids.push(created_node_id); - // `created_node_id` cannot be merged into `parent_node_id`, which may typically be because they are not to the same - // subgraph. However, while `created_node_id` currently depend on `parent_node_id` (directly or indirectly), that - // dependency just come from the fact that `parent_node_id` is the parent of the node whose @requires we're - // dealing with. And in practice, it could well be that some of the fetches needed for that require don't - // really depend on anything that parent fetches and could be done in parallel with it. If we detect that - // this is the case for `created_node_id`, we can move it "up the chain of dependency". + // `created_node_id` cannot be merged into `parent_node_id`, which may typically + // be because they aren't to the same subgraph. However, while `created_node_id` + // currently depends on `parent_node_id` (directly or indirectly), that + // dependency just comes from the fact that `parent_node_id` is the parent of + // the node whose conditions we're dealing with. And in practice, it could well + // be that some of the fetches needed for those conditions don't really depend + // on anything that the parent fetches and could be done in parallel with it. If + // we detect that this is the case for `created_node_id`, we can move it "up the + // chain of dependencies". let mut current_parent = parent.clone(); while dependency_graph.is_child_of_with_artificial_dependency( created_node_id, @@ -4207,10 +4603,10 @@ fn handle_requires( .parents_relations_of(current_parent.parent_node_id) .collect(); if grand_parents.is_empty() { - return Err(FederationError::internal(format!( + bail!( "Fetch node {} is not top-level, so it should have parents", current_parent.parent_node_id.index() - ))); + ); } for grand_parent_relation in &grand_parents { dependency_graph.add_parent( @@ -4236,26 +4632,26 @@ fn handle_requires( } } } else { - // We cannot merge `new_node_id` to the parent, either because there it fetches some things necessary to the - // @requires, or because we had more than one parent and don't know how to handle this (unsure if the later - // can actually happen at this point tbh (?)). But there is no reason not to merge `new_node_id` back to `fetch_node_id` - // so we do that first. - if !dependency_graph.can_merge_sibling_in(fetch_node_id, new_node_id)? { - return Err(FederationError::internal(format!( + // We cannot merge the copied node into the parent because it fetches some conditions + // fields that can't be fetched from the parent. We bail on this specific optimization, + // and accordingly merge the copied node back to the original current node. + if !dependency_graph.can_merge_sibling_in(fetch_node_id, copied_node_id)? { + bail!( "We should be able to merge {} into {} by construction", - new_node_id.index(), + copied_node_id.index(), fetch_node_id.index() - ))); + ); }; - dependency_graph.merge_sibling_in(fetch_node_id, new_node_id)?; - - // The created node depend on `fetch_node` and the dependency cannot be moved to the parent in - // this case. However, we might still be able to merge some created nodes directly in the - // parent. But for this to be true, we should essentially make sure that the dependency - // on `node` is not a "true" dependency. That is, if the created node inputs are the same - // as `node` inputs (and said created node is the same subgraph as the parent of - // `node`, then it means we depend only on values that are already in the parent and - // can merge the node). + dependency_graph.merge_sibling_in(fetch_node_id, copied_node_id)?; + + // The created nodes depend on the current node, and the dependency cannot be moved to + // the parent in this case. However, we might still be able to merge some created nodes + // directly into the parent. But for this to be true, we should essentially make sure + // that the dependency on the current node is not a "true" dependency. That is, if a + // created node's inputs are the same as the current node's inputs (and said created + // node is the same subgraph as the parent of the current node), then it means we depend + // only on values that are already fetched by the parent and/or its ancestors, and + // can merge that created node into the parent. if parent.path_in_parent.is_some() { for created_node_id in newly_created_node_ids { if dependency_graph.can_merge_grand_child_in( @@ -4272,11 +4668,84 @@ fn handle_requires( } } - // If we've merged all the created nodes, then all the "requires" are handled _before_ we get to the - // current node, so we can "continue" with the current node. - if unmerged_node_ids.is_empty() { - // We still need to add the stuffs we require though (but `node` already has a key in its inputs, - // we don't need one). + created_nodes.extend(unmerged_node_ids.clone()); + Ok(ConditionsNodeData { + conditions_merge_node_id: if copied_node_is_unneeded { + parent.parent_node_id + } else { + fetch_node_id + }, + path_in_conditions_merge_node_id: if copied_node_is_unneeded { + parent.path_in_parent + } else { + Some(Arc::new(Default::default())) + }, + created_node_ids: unmerged_node_ids, + is_fully_local_requires: false, + }) + } else { + // We're in the somewhat simpler case where the conditions are queried somewhere in the + // middle of a subgraph fetch (so, not just after having jumped to that subgraph), or + // there's more than one parent. In that case, there isn't much optimisation we can easily + // do, so we leave the nodes as-is. + created_nodes.extend(newly_created_node_ids.clone()); + Ok(ConditionsNodeData { + conditions_merge_node_id: fetch_node_id, + path_in_conditions_merge_node_id: Some(Arc::new(Default::default())), + created_node_ids: newly_created_node_ids.into_iter().collect(), + is_fully_local_requires: false, + }) + } +} + +/// Adds a @requires edge into the node at the given path, instead making a new node if optimization +/// cannot place that edge in the given node. This function assumes handle_conditions_tree() has +/// already been called, and accordingly takes its outputs. +fn create_post_requires_node( + dependency_graph: &mut FetchDependencyGraph, + query_graph_edge_id: EdgeIndex, + (fetch_node_id, fetch_node_path): (NodeIndex, &FetchDependencyGraphNodePath), + context: &OpGraphPathContext, + conditions_node_data: ConditionsNodeData, + created_nodes: &mut IndexSet, +) -> Result<(NodeIndex, FetchDependencyGraphNodePath), FederationError> { + // @requires should be on an entity type, and we only support object types right now. + let head = dependency_graph + .federated_query_graph + .edge_head_weight(query_graph_edge_id)?; + let entity_type_schema = dependency_graph + .federated_query_graph + .schema_by_source(&head.source)? + .clone(); + let QueryGraphNodeType::SchemaType(OutputTypeDefinitionPosition::Object(entity_type_position)) = + head.type_.clone() + else { + bail!("@requires applied on non-entity object type"); + }; + + // If all required fields could be fetched locally, we "continue" with the current node. + if conditions_node_data.is_fully_local_requires { + return Ok((fetch_node_id, fetch_node_path.clone())); + } + + // NOTE: The code paths diverge below similar to handle_conditions_tree(), checking whether we + // tried optimizing based on whether there's a single parent and whether the path in the node is + // only type conditions. This is largely meant to just keep behavior the same as before and be + // aligned with the JS query planner. This could change in the future though, to permit simpler + // handling and further optimization. (There's also some arguably buggy behavior in this + // function we ought to resolve in the future.) + let parent_if_tried_optimizing = + match iter_into_single_item(dependency_graph.parents_relations_of(fetch_node_id)) { + Some(parent) if fetch_node_path.path_in_node.has_only_fragments() => Some(parent), + _ => None, + }; + + if let Some(parent) = parent_if_tried_optimizing { + // If all created nodes were merged into ancestors, then those nodes' data are fetched + // _before_ we get to the current node, so we "continue" with the current node. + if conditions_node_data.created_node_ids.is_empty() { + // We still need to add the required fields as inputs to the current node (but the node + // should already have a key in its inputs, so we don't need to add that). let inputs = inputs_for_require( dependency_graph, entity_type_position.clone(), @@ -4292,51 +4761,51 @@ fn handle_requires( return Ok((fetch_node_id, fetch_node_path.clone())); } - // If we get here, it means that @require needs the information from `unmerged_nodes` (plus whatever has - // been merged before) _and_ those rely on some information from the current `fetch_node` (if they hadn't, we - // would have been able to merge `new_node` to `fetch_node`'s parent). So the group we should return, which - // is the node where the "post-@require" fields will be added, needs to be a new node that depends - // on all those `unmerged_nodes`. - let post_require_node_id = dependency_graph.new_key_node( - &subgraph_name, + // If we get here, it means that @requires needs the fields from the created nodes (plus + // potentially whatever has been merged before). So the node we should return, which is the + // node where the "post-@requires" fields will be given as input, needs to a be a new node + // that depends on all those created nodes. + let fetch_node = dependency_graph.node_weight(fetch_node_id)?; + let target_subgraph = fetch_node.subgraph_name.clone(); + let defer_ref = fetch_node.defer_ref.clone(); + let post_requires_node_id = dependency_graph.new_key_node( + &target_subgraph, fetch_node_path.response_path.clone(), defer_ref, )?; - // Note that `post_require_node` cannot generally be merged in any of the `unmerged_nodes` and we don't provide a `path`. - for unmerged_node_id in &unmerged_node_ids { + // Note that the post-requires node cannot generally be merged into any of the created + // nodes, and we accordingly don't provide a path in those created nodes. + for created_node_id in &conditions_node_data.created_node_ids { dependency_graph.add_parent( - post_require_node_id, + post_requires_node_id, ParentRelation { - parent_node_id: *unmerged_node_id, + parent_node_id: *created_node_id, path_in_parent: None, }, ); } - // That node also need, in general, to depend on the current `fetch_node`. That said, if we detected that the @require - // didn't need anything of said `node` (if `new_node_is_unneeded`), then we can depend on the parent instead. - if new_node_is_not_needed { - dependency_graph.add_parent(post_require_node_id, parent.clone()); - } else { - dependency_graph.add_parent( - post_require_node_id, - ParentRelation { - parent_node_id: fetch_node_id, - path_in_parent: Some(Arc::new(OpPath::default())), - }, - ) - } + // The post-requires node also needs to, in general, depend on the node that the @requires + // conditions were merged into (either the current node or its parent). + dependency_graph.add_parent( + post_requires_node_id, + ParentRelation { + parent_node_id: conditions_node_data.conditions_merge_node_id, + path_in_parent: conditions_node_data.path_in_conditions_merge_node_id, + }, + ); - // Note(Sylvain): I'm not 100% sure about this assert in the sense that while I cannot think of a case where `parent.path_in_parent` wouldn't - // exist, the code paths are complex enough that I'm not able to prove this easily and could easily be missing something. That said, - // we need the path here, so this will have to do for now, and if this ever breaks in practice, we'll at least have an example to - // guide us toward improving/fixing. + // NOTE(Sylvain): I'm not 100% sure about this assert in the sense that while I cannot think + // of a case where `parent.path_in_parent` wouldn't exist, the code paths are complex enough + // that I'm not able to prove this easily and could easily be missing something. That said, + // we need the path here, so this will have to do for now, and if this ever breaks in + // practice, we'll at least have an example to guide us toward improving/fixing the code. let Some(parent_path) = &parent.path_in_parent else { - return Err(FederationError::internal(format!( + bail!( "Missing path_in_parent for @require on {} with group {} and parent {}", query_graph_edge_id.index(), fetch_node_id.index(), parent.parent_node_id.index() - ))); + ); }; let path_for_parent = path_for_parent( dependency_graph, @@ -4352,61 +4821,43 @@ fn handle_requires( query_graph_edge_id, context, parent.parent_node_id, - post_require_node_id, + post_requires_node_id, )?; - created_nodes.extend(unmerged_node_ids); - created_nodes.insert(post_require_node_id); + created_nodes.insert(post_requires_node_id); let initial_fetch_path = create_fetch_initial_path( &dependency_graph.supergraph_schema, &entity_type_position.clone().into(), context, )?; let new_path = fetch_node_path.for_new_key_fetch(initial_fetch_path); - Ok((post_require_node_id, new_path)) + Ok((post_requires_node_id, new_path)) } else { - // We're in the somewhat simpler case where a @require happens somewhere in the middle of a subgraph query (so, not - // just after having jumped to that subgraph). In that case, there isn't tons of optimisation we can do: we have to - // see what satisfying the @require necessitate, and if it needs anything from another subgraph, we have to stop the - // current subgraph fetch there, get the requirements from other subgraphs, and then resume the query of that particular subgraph. - let new_created_nodes = compute_nodes_for_tree( - dependency_graph, - requires_conditions, - fetch_node_id, - fetch_node_path.clone(), - defer_context.for_conditions(), - &OpGraphPathContext::default(), - )?; - // If we didn't create any node, that means the whole condition was fetched from the current node - // and we're good. - if new_created_nodes.is_empty() { - return Ok((fetch_node_id, fetch_node_path.clone())); - } - - // We need to create a new name, on the same subgraph `group`, where we resume fetching the field for - // which we handle the @requires _after_ we've dealt with the `requires_conditions_nodes`. - // Note that we know the conditions will include a key for our node so we can resume properly. + // We need to create a new node on the same subgraph as the current node, where we resume + // fetching the field for which we handle the @requires _after_ we've dealt with any created + // nodes. Note that during option generation, we already ensured a key exists, so the node + // can resume properly. let fetch_node = dependency_graph.node_weight(fetch_node_id)?; let target_subgraph = fetch_node.subgraph_name.clone(); let defer_ref = fetch_node.defer_ref.clone(); - let new_node_id = dependency_graph.new_key_node( + let post_requires_node_id = dependency_graph.new_key_node( &target_subgraph, fetch_node_path.response_path.clone(), defer_ref, )?; - let new_node = dependency_graph.node_weight(new_node_id)?; - let merge_at = new_node.merge_at.clone(); - let parent_type = new_node.parent_type.clone(); - for created_node_id in &new_created_nodes { + let post_requires_node = dependency_graph.node_weight(post_requires_node_id)?; + let merge_at = post_requires_node.merge_at.clone(); + let parent_type = post_requires_node.parent_type.clone(); + for created_node_id in &conditions_node_data.created_node_ids { let created_node = dependency_graph.node_weight(*created_node_id)?; - // Usually, computing the path of our new group into the created groups - // is not entirely trivial, but there is at least the relatively common - // case where the 2 groups we look at have: - // 1) the same `mergeAt`, and - // 2) the same parentType; in that case, we can basically infer those 2 - // groups apply at the same "place" and so the "path in parent" is - // empty. TODO: it should probably be possible to generalize this by - // checking the `mergeAt` plus analyzing the selection but that - // warrants some reflection... + // Usually, computing the path of the post-requires node in the created nodes is not + // entirely trivial, but there is at least one relatively common case where the 2 nodes + // we look at have (1) the same merge-at, and (2) the same parent type. + // + // In that case, we can basically infer those 2 nodes apply at the same "place" and so + // the "path in parent" is empty. + // + // TODO(Sylvain): it should probably be possible to generalize this by checking the + // `merge_at` plus analyzing the selection, but that warrants some reflection... let new_path = if merge_at == created_node.merge_at && parent_type == created_node.parent_type { Some(Arc::new(OpPath::default())) @@ -4417,7 +4868,7 @@ fn handle_requires( parent_node_id: *created_node_id, path_in_parent: new_path, }; - dependency_graph.add_parent(new_node_id, new_parent_relation); + dependency_graph.add_parent(post_requires_node_id, new_parent_relation); } add_post_require_inputs( @@ -4428,17 +4879,16 @@ fn handle_requires( query_graph_edge_id, context, fetch_node_id, - new_node_id, + post_requires_node_id, )?; - created_nodes.extend(new_created_nodes); - created_nodes.insert(new_node_id); + created_nodes.insert(post_requires_node_id); let initial_fetch_path = create_fetch_initial_path( &dependency_graph.supergraph_schema, &entity_type_position.clone().into(), context, )?; let new_path = fetch_node_path.for_new_key_fetch(initial_fetch_path); - Ok((new_node_id, new_path)) + Ok((post_requires_node_id, new_path)) } } @@ -4770,7 +5220,7 @@ mod tests { object: Name, field: Name, ) -> OpPathElement { - OpPathElement::Field(super::Field::new(FieldData { + OpPathElement::Field(Field { schema: schema.clone(), field_position: ObjectTypeDefinitionPosition::new(object) .field(field) @@ -4779,7 +5229,7 @@ mod tests { arguments: Default::default(), directives: Default::default(), sibling_typename: None, - })) + }) } fn inline_fragment_element( @@ -4794,13 +5244,13 @@ mod tests { .unwrap(); let type_condition = type_condition_name.map(|n| schema.get_type(n).unwrap().try_into().unwrap()); - OpPathElement::InlineFragment(super::InlineFragment::new(InlineFragmentData { + OpPathElement::InlineFragment(InlineFragment { schema: schema.clone(), parent_type_position: parent_type, type_condition_position: type_condition, directives: Default::default(), selection_id: SelectionId::new(), - })) + }) } fn to_string(response_path: &[FetchDataPathElement]) -> String { @@ -4818,6 +5268,9 @@ mod tests { FetchDataPathElement::TypenameEquals(_) => { unimplemented!() } + FetchDataPathElement::Parent => { + unimplemented!() + } }) .join(".") ) diff --git a/apollo-federation/src/query_plan/mod.rs b/apollo-federation/src/query_plan/mod.rs index d8c90c2c4a..313b9f6322 100644 --- a/apollo-federation/src/query_plan/mod.rs +++ b/apollo-federation/src/query_plan/mod.rs @@ -88,7 +88,8 @@ pub struct FetchNode { /// received from a fetch (and before it is applied to the current in-memory results). pub output_rewrites: Vec>, /// Similar to the other kinds of rewrites. This is a mechanism to convert a contextual path into - /// an argument to a resolver + /// an argument to a resolver. Note value setters are currently unused here, but may be used in + /// the future. pub context_rewrites: Vec>, } @@ -193,7 +194,7 @@ pub struct ConditionNode { /// /// A rewrite usually identifies some sub-part of the data and some action to perform on that /// sub-part. -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize, derive_more::From)] pub enum FetchDataRewrite { ValueSetter(FetchDataValueSetter), KeyRenamer(FetchDataKeyRenamer), @@ -219,9 +220,10 @@ pub struct FetchDataKeyRenamer { } /// Vectors of this element match path(s) to a value in fetch data. Each element is (1) a key in -/// object data, (2) _any_ index in array data (often serialized as `@`), or (3) a typename -/// constraint on the object data at that point in the path(s) (a path should only match for objects -/// whose `__typename` is the provided type). +/// object data, (2) _any_ index in array data (often serialized as `@`), (3) a typename constraint +/// on the object data at that point in the path(s) (a path should only match for objects whose +/// `__typename` is the provided type), or (4) a parent indicator to move upwards one level in the +/// object. /// /// It's possible for vectors of this element to match no paths in fetch data, e.g. if an object key /// doesn't exist, or if an object's `__typename` doesn't equal the provided one. If this occurs, @@ -238,6 +240,7 @@ pub enum FetchDataPathElement { Key(Name, Conditions), AnyIndex(Conditions), TypenameEquals(Name), + Parent, } pub type Conditions = Vec; diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index 500076ce4c..ed2a4b2589 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -3,7 +3,6 @@ use std::num::NonZeroU32; use std::ops::Deref; use std::sync::Arc; -use apollo_compiler::collections::HashSet; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::validation::Valid; @@ -11,6 +10,7 @@ use apollo_compiler::ExecutableDocument; use apollo_compiler::Name; use itertools::Itertools; use serde::Serialize; +use tracing::trace; use super::fetch_dependency_graph::FetchIdGenerator; use super::ConditionNode; @@ -51,35 +51,28 @@ use crate::utils::logging::snapshot; use crate::ApiSchemaOptions; use crate::Supergraph; -pub(crate) const CONTEXT_DIRECTIVE: &str = "context"; - -#[derive(Debug, Clone, Hash)] +#[derive(Debug, Clone, Hash, Serialize)] pub struct QueryPlannerConfig { - /// Whether the query planner should try to reused the named fragments of the planned query in + /// Whether the query planner should try to reuse the named fragments of the planned query in /// subgraph fetches. /// - /// This is often a good idea as it can prevent very large subgraph queries in some cases (named - /// fragments can make some relatively small queries (using said fragments) expand to a very large - /// query if all the spreads are inline). However, due to architecture of the query planner, this - /// optimization is done as an additional pass on the subgraph queries of the generated plan and - /// can thus increase the latency of building a plan. As long as query plans are sufficiently - /// cached, this should not be a problem, which is why this option is enabled by default, but if - /// the distribution of inbound queries prevents efficient caching of query plans, this may become - /// an undesirable trade-off and can be disabled in that case. + /// Reusing fragments requires complicated validations, so it can take a long time on large + /// queries with many fragments. This option may be removed in the future in favour of + /// [`generate_query_fragments`][QueryPlannerConfig::generate_query_fragments]. /// - /// Defaults to true. + /// Defaults to false. pub reuse_query_fragments: bool, - /// NOTE: **not implemented yet** - /// /// If enabled, the query planner will extract inline fragments into fragment /// definitions before sending queries to subgraphs. This can significantly - /// reduce the size of the query sent to subgraphs, but may increase the time - /// it takes to plan the query. + /// reduce the size of the query sent to subgraphs. /// /// Defaults to false. pub generate_query_fragments: bool, + /// **TODO:** This option is not implemented, and the behaviour is *always enabled*. + /// + /// /// Whether to run GraphQL validation against the extracted subgraph schemas. Recommended in /// non-production settings or when debugging. /// @@ -107,31 +100,36 @@ pub struct QueryPlannerConfig { pub type_conditioned_fetching: bool, } +#[allow(clippy::derivable_impls)] // it's derivable right now, but we might change the defaults impl Default for QueryPlannerConfig { fn default() -> Self { Self { - reuse_query_fragments: true, - subgraph_graphql_validation: false, + reuse_query_fragments: false, generate_query_fragments: false, + subgraph_graphql_validation: false, incremental_delivery: Default::default(), debug: Default::default(), - type_conditioned_fetching: Default::default(), + type_conditioned_fetching: false, } } } -#[derive(Debug, Clone, Default, Hash)] +#[derive(Debug, Clone, Default, Hash, Serialize)] pub struct QueryPlanIncrementalDeliveryConfig { - /// Enables @defer support by the query planner. + /// Enables `@defer` support in the query planner, breaking up the query plan with [DeferNode]s + /// as appropriate. + /// + /// If false, operations with `@defer` are still accepted, but are planned as if they did not + /// contain `@defer` directives. /// - /// If set, then the query plan for queries having some @defer will contains some `DeferNode` - /// (see `query_plan/mod.rs`). + /// Defaults to false. /// - /// Defaults to false (meaning that the @defer are ignored). + /// [DeferNode]: crate::query_plan::DeferNode + #[serde(default)] pub enable_defer: bool, } -#[derive(Debug, Clone, Hash)] +#[derive(Debug, Clone, Hash, Serialize)] pub struct QueryPlannerDebugConfig { /// If used and the supergraph is built from a single subgraph, then user queries do not go /// through the normal query planning and instead a fetch to the one subgraph is built directly @@ -209,10 +207,10 @@ pub struct QueryPlanOptions { } #[derive(Debug, Default, Clone)] -pub(crate) struct EnabledOverrideConditions(HashSet); +pub(crate) struct EnabledOverrideConditions(IndexSet); impl Deref for EnabledOverrideConditions { - type Target = HashSet; + type Target = IndexSet; fn deref(&self) -> &Self::Target { &self.0 @@ -244,7 +242,6 @@ impl QueryPlanner { config: QueryPlannerConfig, ) -> Result { config.assert_valid(); - Self::check_unsupported_features(supergraph)?; let supergraph_schema = supergraph.schema.clone(); let api_schema = supergraph.to_api_schema(ApiSchemaOptions { @@ -488,7 +485,7 @@ impl QueryPlanner { .clone() .into(), config: self.config.clone(), - override_conditions: EnabledOverrideConditions(HashSet::from_iter( + override_conditions: EnabledOverrideConditions(IndexSet::from_iter( options.override_conditions, )), fetch_id_generator: Arc::new(FetchIdGenerator::new()), @@ -551,7 +548,15 @@ impl QueryPlanner { statistics, }; - snapshot!(plan, "query plan"); + snapshot!( + "QueryPlan", + plan.to_string(), + "QueryPlan from build_query_plan" + ); + snapshot!( + plan.statistics, + "QueryPlanningStatistics from build_query_plan" + ); Ok(plan) } @@ -560,36 +565,6 @@ impl QueryPlanner { pub fn api_schema(&self) -> &ValidFederationSchema { &self.api_schema } - - fn check_unsupported_features(supergraph: &Supergraph) -> Result<(), FederationError> { - // We will only check for `@context` direcive, since - // `@fromContext` can only be used if `@context` is already - // applied, and we assume a correctly composed supergraph. - // - // `@context` can only be applied on Object Types, Interface - // Types and Unions. For simplicity of this function, we just - // check all 'extended_type` directives. - let has_set_context = supergraph - .schema - .schema() - .types - .values() - .any(|extended_type| extended_type.directives().has(CONTEXT_DIRECTIVE)); - if has_set_context { - let message = "\ - `experimental_query_planner_mode: new` or `both` cannot yet \ - be used with `@context`. \ - Remove uses of `@context` to try the experimental query planner, \ - otherwise switch back to `legacy` or `both_best_effort`.\ - "; - return Err(SingleFederationError::UnsupportedFeature { - message: message.to_owned(), - kind: crate::error::UnsupportedFeatureKind::Context, - } - .into()); - } - Ok(()) - } } fn compute_root_serial_dependency_graph( @@ -716,7 +691,11 @@ pub(crate) fn compute_root_fetch_groups( root_kind, root_type.clone(), )?; - snapshot!(dependency_graph, "tree_with_root_node"); + snapshot!( + "FetchDependencyGraph", + dependency_graph.to_dot(), + "tree_with_root_node" + ); compute_nodes_for_tree( dependency_graph, &child.tree, @@ -737,16 +716,13 @@ fn compute_root_parallel_dependency_graph( parameters: &QueryPlanningParameters, has_defers: bool, ) -> Result { - snapshot!( - "FetchDependencyGraph", - "Empty", - "Starting process to construct a parallel fetch dependency graph" - ); + trace!("Starting process to construct a parallel fetch dependency graph"); let selection_set = parameters.operation.selection_set.clone(); let best_plan = compute_root_parallel_best_plan(parameters, selection_set, has_defers)?; snapshot!( - best_plan.fetch_dependency_graph, - "Plan returned from compute_root_parallel_best_plan" + "FetchDependencyGraph", + best_plan.fetch_dependency_graph.to_dot(), + "Fetch dependency graph returned from compute_root_parallel_best_plan" ); Ok(best_plan.fetch_dependency_graph) } @@ -807,7 +783,8 @@ fn compute_plan_internal( let (main, deferred) = dependency_graph.process(&mut *processor, root_kind)?; snapshot!( - dependency_graph, + "FetchDependencyGraph", + dependency_graph.to_dot(), "Plan after calling FetchDependencyGraph::process" ); // XXX(@goto-bus-stop) Maybe `.defer_tracking` should be on the return value of `process()`..? @@ -1372,7 +1349,11 @@ type User ) .unwrap(); - let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap(); + let config = QueryPlannerConfig { + reuse_query_fragments: true, + ..Default::default() + }; + let planner = QueryPlanner::new(&supergraph, config).unwrap(); let plan = planner .build_query_plan(&document, None, Default::default()) .unwrap(); @@ -1433,7 +1414,11 @@ type User ) .unwrap(); - let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap(); + let config = QueryPlannerConfig { + reuse_query_fragments: true, + ..Default::default() + }; + let planner = QueryPlanner::new(&supergraph, config).unwrap(); let plan = planner .build_query_plan(&document, None, Default::default()) .unwrap(); @@ -1495,7 +1480,11 @@ type User ) .unwrap(); - let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap(); + let config = QueryPlannerConfig { + reuse_query_fragments: true, + ..Default::default() + }; + let planner = QueryPlanner::new(&supergraph, config).unwrap(); let plan = planner .build_query_plan(&document, None, Default::default()) .unwrap(); diff --git a/apollo-federation/src/query_plan/query_planning_traversal.rs b/apollo-federation/src/query_plan/query_planning_traversal.rs index 787013471a..21585e1b73 100644 --- a/apollo-federation/src/query_plan/query_planning_traversal.rs +++ b/apollo-federation/src/query_plan/query_planning_traversal.rs @@ -46,8 +46,17 @@ use crate::schema::position::CompositeTypeDefinitionPosition; use crate::schema::position::ObjectTypeDefinitionPosition; use crate::schema::position::SchemaRootDefinitionKind; use crate::schema::ValidFederationSchema; +use crate::utils::logging::format_open_branch; use crate::utils::logging::snapshot; +#[cfg(feature = "snapshot_tracing")] +mod snapshot_helper { + // A module to import functions only used within `snapshot!(...)` macros. + pub(crate) use crate::utils::logging::closed_branches_to_string; + pub(crate) use crate::utils::logging::open_branch_to_string; + pub(crate) use crate::utils::logging::open_branches_to_string; +} + // PORT_NOTE: Named `PlanningParameters` in the JS codebase, but there was no particular reason to // leave out to the `Query` prefix, so it's been added for consistency. Similar to `GraphPath`, we // don't have a distinguished type for when the head is a root vertex, so we instead check this at @@ -113,13 +122,33 @@ pub(crate) struct QueryPlanningTraversal<'a, 'b> { } #[derive(Debug, Serialize)] -struct OpenBranchAndSelections { +pub(crate) struct OpenBranchAndSelections { /// The options for this open branch. open_branch: OpenBranch, /// A stack of the remaining selections to plan from the node this open branch ends on. selections: Vec, } +impl std::fmt::Display for OpenBranchAndSelections { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Some((current_selection, remaining_selections)) = self.selections.split_last() else { + return Ok(()); + }; + format_open_branch(f, &(current_selection, &self.open_branch.0))?; + write!(f, " * Remaining selections:")?; + if remaining_selections.is_empty() { + writeln!(f, " (none)")?; + } else { + // Print in reverse order since remaining selections are processed in that order. + writeln!(f)?; // newline + for selection in remaining_selections.iter().rev() { + writeln!(f, " - {selection}")?; + } + } + Ok(()) + } +} + struct PlanInfo { fetch_dependency_graph: FetchDependencyGraph, path_tree: Arc, @@ -284,7 +313,17 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { ) )] fn find_best_plan_inner(&mut self) -> Result, FederationError> { - while let Some(mut current_branch) = self.open_branches.pop() { + while !self.open_branches.is_empty() { + snapshot!( + "OpenBranches", + snapshot_helper::open_branches_to_string(&self.open_branches), + "Query planning open branches" + ); + let Some(mut current_branch) = self.open_branches.pop() else { + return Err(FederationError::internal( + "Branch stack unexpectedly empty during query plan traversal", + )); + }; let Some(current_selection) = current_branch.selections.pop() else { return Err(FederationError::internal( "Sub-stack unexpectedly empty during query plan traversal", @@ -293,7 +332,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { let (terminate_planning, new_branch) = self.handle_open_branch(¤t_selection, &mut current_branch.open_branch.0)?; if terminate_planning { - trace!("Planning termianted!"); + trace!("Planning terminated!"); // We clear both open branches and closed ones as a means to terminate the plan // computation with no plan. self.open_branches = vec![]; @@ -330,12 +369,10 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { let mut new_options = vec![]; let mut no_followups: bool = false; - snapshot!(name = "Options", options, "options"); - snapshot!( - "OperationElement", - operation_element.to_string(), - "operation_element" + "OpenBranch", + snapshot_helper::open_branch_to_string(selection, options), + "open branch" ); for option in options.iter_mut() { @@ -368,7 +405,11 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { } } - snapshot!(new_options, "new_options"); + snapshot!( + "OpenBranch", + snapshot_helper::open_branch_to_string(selection, &new_options), + "new_options" + ); if no_followups { // This operation element is valid from this option, but is guarantee to yield no result @@ -610,8 +651,8 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { )] fn compute_best_plan_from_closed_branches(&mut self) -> Result<(), FederationError> { snapshot!( - name = "ClosedBranches", - self.closed_branches, + "ClosedBranches", + snapshot_helper::closed_branches_to_string(&self.closed_branches), "closed_branches" ); @@ -622,8 +663,8 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { self.reduce_options_if_needed(); snapshot!( - name = "ClosedBranches", - self.closed_branches, + "ClosedBranches", + snapshot_helper::closed_branches_to_string(&self.closed_branches), "closed_branches_after_reduce" ); @@ -653,7 +694,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { let (first_group, second_group) = self.closed_branches.split_at(sole_path_branch_index); let initial_tree; - snapshot!("FetchDependencyGraph", "", "Generating initial dep graph"); + trace!("Generating initial fetch dependency graph"); let mut initial_dependency_graph = self.new_dependency_graph(); let federated_query_graph = &self.parameters.federated_query_graph; let root = &self.parameters.head; @@ -678,21 +719,32 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { self.parameters.config.type_conditioned_fetching, )?; snapshot!( - initial_dependency_graph, + "FetchDependencyGraph", + initial_dependency_graph.to_dot(), "Updated dep graph with initial tree" ); if first_group.is_empty() { // Well, we have the only possible plan; it's also the best. let cost = self.cost(&mut initial_dependency_graph)?; - self.best_plan = BestQueryPlanInfo { + let best_plan = BestQueryPlanInfo { fetch_dependency_graph: initial_dependency_graph, path_tree: initial_tree.into(), cost, - } - .into(); + }; - snapshot!(self.best_plan, "best_plan"); + snapshot!( + "FetchDependencyGraph", + best_plan.fetch_dependency_graph.to_dot(), + "best_plan.fetch_dependency_graph" + ); + snapshot!( + "OpPathTree", + best_plan.path_tree.to_string(), + "best_plan.path_tree" + ); + snapshot!(best_plan.cost, "best_plan.cost"); + self.best_plan = best_plan.into(); return Ok(()); } } @@ -723,14 +775,25 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { other_trees, /*plan_builder*/ self, )?; - self.best_plan = BestQueryPlanInfo { + let best_plan = BestQueryPlanInfo { fetch_dependency_graph: best.fetch_dependency_graph, path_tree: best.path_tree, cost, - } - .into(); + }; + + snapshot!( + "FetchDependencyGraph", + best_plan.fetch_dependency_graph.to_dot(), + "best_plan.fetch_dependency_graph" + ); + snapshot!( + "OpPathTree", + best_plan.path_tree.to_string(), + "best_plan.path_tree" + ); + snapshot!(best_plan.cost, "best_plan.cost"); - snapshot!(self.best_plan, "best_plan"); + self.best_plan = best_plan.into(); Ok(()) } @@ -975,7 +1038,11 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { )?; } - snapshot!(dependency_graph, "updated_dependency_graph"); + snapshot!( + "FetchDependencyGraph", + dependency_graph.to_dot(), + "updated_dependency_graph" + ); Ok(()) } @@ -993,17 +1060,16 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { context: &OpGraphPathContext, excluded_destinations: &ExcludedDestinations, excluded_conditions: &ExcludedConditions, + extra_conditions: Option<&SelectionSet>, ) -> Result { let graph = &self.parameters.federated_query_graph; let head = graph.edge_endpoints(edge)?.0; // Note: `QueryPlanningTraversal::resolve` method asserts that the edge has conditions before // calling this method. - let edge_conditions = graph - .edge_weight(edge)? - .conditions - .as_ref() - .unwrap() - .as_ref(); + let edge_conditions = match extra_conditions { + Some(set) => set, + None => graph.edge_weight(edge)?.conditions.as_ref().unwrap(), + }; let parameters = QueryPlanningParameters { head, head_must_be_root: graph.node_weight(head)?.is_root_node(), @@ -1037,6 +1103,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { Some(best_plan) => Ok(ConditionResolution::Satisfied { cost: best_plan.cost, path_tree: Some(best_plan.path_tree), + context_map: None, }), None => Ok(ConditionResolution::unsatisfied_conditions()), } @@ -1102,31 +1169,42 @@ impl<'a: 'b, 'b> PlanBuilder> for QueryPlanningTravers // implement `ConditionResolver` trait along with `resolver_cache` field. impl<'a> ConditionResolver for QueryPlanningTraversal<'a, '_> { /// A query plan resolver for edge conditions that caches the outcome per edge. + #[track_caller] fn resolve( &mut self, edge: EdgeIndex, context: &OpGraphPathContext, excluded_destinations: &ExcludedDestinations, excluded_conditions: &ExcludedConditions, + extra_conditions: Option<&SelectionSet>, ) -> Result { // Invariant check: The edge must have conditions. let graph = &self.parameters.federated_query_graph; let edge_data = graph.edge_weight(edge)?; assert!( - edge_data.conditions.is_some(), + edge_data.conditions.is_some() || extra_conditions.is_some(), "Should not have been called for edge without conditions" ); - let cache_result = - self.resolver_cache - .contains(edge, context, excluded_destinations, excluded_conditions); + let cache_result = self.resolver_cache.contains( + edge, + context, + excluded_destinations, + excluded_conditions, + extra_conditions, + ); if let ConditionResolutionCacheResult::Hit(cached_resolution) = cache_result { return Ok(cached_resolution); } - let resolution = - self.resolve_condition_plan(edge, context, excluded_destinations, excluded_conditions)?; + let resolution = self.resolve_condition_plan( + edge, + context, + excluded_destinations, + excluded_conditions, + extra_conditions, + )?; // See if this resolution is eligible to be inserted into the cache. if cache_result.is_miss() { self.resolver_cache diff --git a/apollo-federation/src/schema/position.rs b/apollo-federation/src/schema/position.rs index 99701a5db1..377968542e 100644 --- a/apollo-federation/src/schema/position.rs +++ b/apollo-federation/src/schema/position.rs @@ -4,7 +4,6 @@ use std::fmt::Formatter; use std::ops::Deref; use apollo_compiler::ast; -use apollo_compiler::collections::IndexSet; use apollo_compiler::name; use apollo_compiler::schema::Component; use apollo_compiler::schema::ComponentName; @@ -25,10 +24,10 @@ use apollo_compiler::Name; use apollo_compiler::Node; use apollo_compiler::Schema; use either::Either; -use lazy_static::lazy_static; use serde::Serialize; use strum::IntoEnumIterator; +use crate::bail; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::link::database::links_metadata; @@ -1050,16 +1049,7 @@ impl ScalarTypeDefinitionPosition { pub(crate) fn pre_insert(&self, schema: &mut FederationSchema) -> Result<(), FederationError> { if schema.referencers.contains_type_name(&self.type_name) { - // TODO: Allow built-in shadowing instead of ignoring them - if is_graphql_reserved_name(&self.type_name) - || GRAPHQL_BUILTIN_SCALAR_NAMES.contains(&self.type_name) - { - return Ok(()); - } - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" has already been pre-inserted", self), - } - .into()); + bail!(r#"Type "{self}" has already been pre-inserted"#); } schema .referencers @@ -1087,22 +1077,10 @@ impl ScalarTypeDefinitionPosition { .scalar_types .contains_key(&self.type_name) { - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" has not been pre-inserted", self), - } - .into()); + bail!(r#"Type "{self}" has not been pre-inserted"#); } if schema.schema.types.contains_key(&self.type_name) { - // TODO: Allow built-in shadowing instead of ignoring them - if is_graphql_reserved_name(&self.type_name) - || GRAPHQL_BUILTIN_SCALAR_NAMES.contains(&self.type_name) - { - return Ok(()); - } - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" already exists in schema", self), - } - .into()); + bail!(r#"Type "{self}" already exists in schema"#); } schema .schema @@ -1354,14 +1332,7 @@ impl ObjectTypeDefinitionPosition { pub(crate) fn pre_insert(&self, schema: &mut FederationSchema) -> Result<(), FederationError> { if schema.referencers.contains_type_name(&self.type_name) { - // TODO: Allow built-in shadowing instead of ignoring them - if is_graphql_reserved_name(&self.type_name) { - return Ok(()); - } - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" has already been pre-inserted", self), - } - .into()); + bail!(r#"Type "{self}" has already been pre-inserted"#); } schema .referencers @@ -1389,20 +1360,10 @@ impl ObjectTypeDefinitionPosition { .object_types .contains_key(&self.type_name) { - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" has not been pre-inserted", self), - } - .into()); + bail!(r#"Type "{self}" has not been pre-inserted"#); } if schema.schema.types.contains_key(&self.type_name) { - // TODO: Allow built-in shadowing instead of ignoring them - if is_graphql_reserved_name(&self.type_name) { - return Ok(()); - } - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" already exists in schema", self), - } - .into()); + bail!(r#"Type "{self}" already exists in schema"#); } schema .schema @@ -1824,10 +1785,7 @@ impl ObjectFieldDefinitionPosition { .into()); } if self.try_get(&schema.schema).is_some() { - return Err(SingleFederationError::Internal { - message: format!("Object field \"{}\" already exists in schema", self), - } - .into()); + bail!(r#"Object field "{self}" already exists in schema"#); } self.parent() .make_mut(&mut schema.schema)? @@ -1936,10 +1894,7 @@ impl ObjectFieldDefinitionPosition { allow_built_ins: bool, ) -> Result<(), FederationError> { if !allow_built_ins && is_graphql_reserved_name(&self.field_name) { - return Err(SingleFederationError::Internal { - message: format!("Cannot insert reserved object field \"{}\"", self), - } - .into()); + bail!(r#"Cannot insert reserved object field "{self}""#); } validate_node_directives(field.directives.deref())?; for directive_reference in field.directives.iter() { @@ -1961,10 +1916,7 @@ impl ObjectFieldDefinitionPosition { allow_built_ins: bool, ) -> Result<(), FederationError> { if !allow_built_ins && is_graphql_reserved_name(&self.field_name) { - return Err(SingleFederationError::Internal { - message: format!("Cannot remove reserved object field \"{}\"", self), - } - .into()); + bail!(r#"Cannot remove reserved object field "{self}""#); } for directive_reference in field.directives.iter() { self.remove_directive_name_references(referencers, &directive_reference.name); @@ -2086,7 +2038,7 @@ impl Debug for ObjectFieldDefinitionPosition { } } -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub(crate) struct ObjectFieldArgumentDefinitionPosition { pub(crate) type_name: Name, pub(crate) field_name: Name, @@ -2190,10 +2142,7 @@ impl ObjectFieldArgumentDefinitionPosition { referencers: &mut Referencers, ) -> Result<(), FederationError> { if is_graphql_reserved_name(&self.argument_name) { - return Err(SingleFederationError::Internal { - message: format!("Cannot insert reserved object field argument \"{}\"", self), - } - .into()); + bail!(r#"Cannot insert reserved object field argument "{self}""#); } validate_node_directives(argument.directives.deref())?; for directive_reference in argument.directives.iter() { @@ -2208,10 +2157,7 @@ impl ObjectFieldArgumentDefinitionPosition { referencers: &mut Referencers, ) -> Result<(), FederationError> { if is_graphql_reserved_name(&self.argument_name) { - return Err(SingleFederationError::Internal { - message: format!("Cannot remove reserved object field argument \"{}\"", self), - } - .into()); + bail!(r#"Cannot remove reserved object field argument "{self}""#); } for directive_reference in argument.directives.iter() { self.remove_directive_name_references(referencers, &directive_reference.name); @@ -2423,14 +2369,7 @@ impl InterfaceTypeDefinitionPosition { pub(crate) fn pre_insert(&self, schema: &mut FederationSchema) -> Result<(), FederationError> { if schema.referencers.contains_type_name(&self.type_name) { - // TODO: Allow built-in shadowing instead of ignoring them - if is_graphql_reserved_name(&self.type_name) { - return Ok(()); - } - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" has already been pre-inserted", self), - } - .into()); + bail!(r#"Type "{self}" has already been pre-inserted"#); } schema .referencers @@ -2458,20 +2397,10 @@ impl InterfaceTypeDefinitionPosition { .interface_types .contains_key(&self.type_name) { - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" has not been pre-inserted", self), - } - .into()); + bail!(r#"Type "{self}" has not been pre-inserted"#); } if schema.schema.types.contains_key(&self.type_name) { - // TODO: Allow built-in shadowing instead of ignoring them - if is_graphql_reserved_name(&self.type_name) { - return Ok(()); - } - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" already exists in schema", self), - } - .into()); + bail!(r#"Type "{self}" already exists in schema"#); } schema .schema @@ -2823,10 +2752,7 @@ impl InterfaceFieldDefinitionPosition { .into()); } if self.try_get(&schema.schema).is_some() { - return Err(SingleFederationError::Internal { - message: format!("Interface field \"{}\" already exists in schema", self), - } - .into()); + bail!(r#"Interface field "{self}" already exists in schema"#); } self.parent() .make_mut(&mut schema.schema)? @@ -2935,10 +2861,7 @@ impl InterfaceFieldDefinitionPosition { allow_built_ins: bool, ) -> Result<(), FederationError> { if !allow_built_ins && is_graphql_reserved_name(&self.field_name) { - return Err(SingleFederationError::Internal { - message: format!("Cannot insert reserved interface field \"{}\"", self), - } - .into()); + bail!(r#"Cannot insert reserved interface field "{self}""#); } validate_node_directives(field.directives.deref())?; for directive_reference in field.directives.iter() { @@ -2960,10 +2883,7 @@ impl InterfaceFieldDefinitionPosition { allow_built_ins: bool, ) -> Result<(), FederationError> { if !allow_built_ins && is_graphql_reserved_name(&self.field_name) { - return Err(SingleFederationError::Internal { - message: format!("Cannot remove reserved interface field \"{}\"", self), - } - .into()); + bail!(r#"Cannot remove reserved interface field "{self}""#); } for directive_reference in field.directives.iter() { self.remove_directive_name_references(referencers, &directive_reference.name); @@ -3188,13 +3108,7 @@ impl InterfaceFieldArgumentDefinitionPosition { referencers: &mut Referencers, ) -> Result<(), FederationError> { if is_graphql_reserved_name(&self.argument_name) { - return Err(SingleFederationError::Internal { - message: format!( - "Cannot insert reserved interface field argument \"{}\"", - self - ), - } - .into()); + bail!(r#"Cannot insert reserved interface field argument "{self}""#); } validate_node_directives(argument.directives.deref())?; for directive_reference in argument.directives.iter() { @@ -3209,13 +3123,7 @@ impl InterfaceFieldArgumentDefinitionPosition { referencers: &mut Referencers, ) -> Result<(), FederationError> { if is_graphql_reserved_name(&self.argument_name) { - return Err(SingleFederationError::Internal { - message: format!( - "Cannot remove reserved interface field argument \"{}\"", - self - ), - } - .into()); + bail!(r#"Cannot remove reserved interface field argument "{self}""#); } for directive_reference in argument.directives.iter() { self.remove_directive_name_references(referencers, &directive_reference.name); @@ -3401,14 +3309,7 @@ impl UnionTypeDefinitionPosition { pub(crate) fn pre_insert(&self, schema: &mut FederationSchema) -> Result<(), FederationError> { if schema.referencers.contains_type_name(&self.type_name) { - // TODO: Allow built-in shadowing instead of ignoring them - if is_graphql_reserved_name(&self.type_name) { - return Ok(()); - } - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" has already been pre-inserted", self), - } - .into()); + bail!(r#"Type "{self}" has already been pre-inserted"#); } schema .referencers @@ -3432,20 +3333,10 @@ impl UnionTypeDefinitionPosition { .into()); } if !schema.referencers.union_types.contains_key(&self.type_name) { - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" has not been pre-inserted", self), - } - .into()); + bail!(r#"Type "{self}" has not been pre-inserted"#); } if schema.schema.types.contains_key(&self.type_name) { - // TODO: Allow built-in shadowing instead of ignoring them - if is_graphql_reserved_name(&self.type_name) { - return Ok(()); - } - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" already exists in schema", self), - } - .into()); + bail!(r#"Type "{self}" already exists in schema"#); } schema .schema @@ -3815,14 +3706,7 @@ impl EnumTypeDefinitionPosition { pub(crate) fn pre_insert(&self, schema: &mut FederationSchema) -> Result<(), FederationError> { if schema.referencers.contains_type_name(&self.type_name) { - // TODO: Allow built-in shadowing instead of ignoring them - if is_graphql_reserved_name(&self.type_name) { - return Ok(()); - } - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" has already been pre-inserted", self), - } - .into()); + bail!(r#"Type "{self}" has already been pre-inserted"#); } schema .referencers @@ -3843,20 +3727,10 @@ impl EnumTypeDefinitionPosition { .into()); } if !schema.referencers.enum_types.contains_key(&self.type_name) { - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" has not been pre-inserted", self), - } - .into()); + bail!(r#"Type "{self}" has not been pre-inserted"#); } if schema.schema.types.contains_key(&self.type_name) { - // TODO: Allow built-in shadowing instead of ignoring them - if is_graphql_reserved_name(&self.type_name) { - return Ok(()); - } - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" already exists in schema", self), - } - .into()); + bail!(r#"Type "{self}" already exists in schema"#); } schema .schema @@ -4088,10 +3962,7 @@ impl EnumValueDefinitionPosition { .into()); } if self.try_get(&schema.schema).is_some() { - return Err(SingleFederationError::Internal { - message: format!("Enum value \"{}\" already exists in schema", self,), - } - .into()); + bail!(r#"Enum value "{self}" already exists in schema"#); } self.parent() .make_mut(&mut schema.schema)? @@ -4136,9 +4007,7 @@ impl EnumValueDefinitionPosition { referencers: &mut Referencers, ) -> Result<(), FederationError> { if is_graphql_reserved_name(&self.value_name) { - return Err(FederationError::internal(format!( - "Cannot insert reserved enum value \"{self}\"" - ))); + bail!(r#"Cannot insert reserved enum value "{self}""#); } validate_node_directives(value.directives.deref())?; for directive_reference in value.directives.iter() { @@ -4153,10 +4022,7 @@ impl EnumValueDefinitionPosition { referencers: &mut Referencers, ) -> Result<(), FederationError> { if is_graphql_reserved_name(&self.value_name) { - return Err(SingleFederationError::Internal { - message: format!("Cannot remove reserved enum value \"{}\"", self), - } - .into()); + bail!(r#"Cannot remove reserved enum value "{self}""#); } for directive_reference in value.directives.iter() { self.remove_directive_name_references(referencers, &directive_reference.name); @@ -4271,14 +4137,7 @@ impl InputObjectTypeDefinitionPosition { pub(crate) fn pre_insert(&self, schema: &mut FederationSchema) -> Result<(), FederationError> { if schema.referencers.contains_type_name(&self.type_name) { - // TODO: Allow built-in shadowing instead of ignoring them - if is_graphql_reserved_name(&self.type_name) { - return Ok(()); - } - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" has already been pre-inserted", self), - } - .into()); + bail!(r#"Type "{self}" has already been pre-inserted"#); } schema .referencers @@ -4306,20 +4165,10 @@ impl InputObjectTypeDefinitionPosition { .input_object_types .contains_key(&self.type_name) { - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" has not been pre-inserted", self), - } - .into()); + bail!(r#"Type "{self}" has not been pre-inserted"#); } if schema.schema.types.contains_key(&self.type_name) { - // TODO: Allow built-in shadowing instead of ignoring them - if is_graphql_reserved_name(&self.type_name) { - return Ok(()); - } - return Err(SingleFederationError::Internal { - message: format!("Type \"{}\" already exists in schema", self), - } - .into()); + bail!(r#"Type "{self}" already exists in schema"#); } schema .schema @@ -4578,10 +4427,7 @@ impl InputObjectFieldDefinitionPosition { .into()); } if self.try_get(&schema.schema).is_some() { - return Err(SingleFederationError::Internal { - message: format!("Input object field \"{}\" already exists in schema", self), - } - .into()); + bail!(r#"Input object field "{self}" already exists in schema"#); } self.parent() .make_mut(&mut schema.schema)? @@ -4643,10 +4489,7 @@ impl InputObjectFieldDefinitionPosition { referencers: &mut Referencers, ) -> Result<(), FederationError> { if is_graphql_reserved_name(&self.field_name) { - return Err(SingleFederationError::Internal { - message: format!("Cannot insert reserved input object field \"{}\"", self), - } - .into()); + bail!(r#"Cannot insert reserved input object field "{self}""#); } validate_node_directives(field.directives.deref())?; for directive_reference in field.directives.iter() { @@ -4661,10 +4504,7 @@ impl InputObjectFieldDefinitionPosition { referencers: &mut Referencers, ) -> Result<(), FederationError> { if is_graphql_reserved_name(&self.field_name) { - return Err(SingleFederationError::Internal { - message: format!("Cannot remove reserved input object field \"{}\"", self), - } - .into()); + bail!(r#"Cannot remove reserved input object field "{self}""#); } for directive_reference in field.directives.iter() { self.remove_directive_name_references(referencers, &directive_reference.name); @@ -4814,16 +4654,7 @@ impl DirectiveDefinitionPosition { .directives .contains_key(&self.directive_name) { - // TODO: Allow built-in shadowing instead of ignoring them - if is_graphql_reserved_name(&self.directive_name) - || GRAPHQL_BUILTIN_DIRECTIVE_NAMES.contains(&self.directive_name) - { - return Ok(()); - } - return Err(SingleFederationError::Internal { - message: format!("Directive \"{}\" has already been pre-inserted", self), - } - .into()); + bail!(r#"Directive "{self}" has already been pre-inserted"#); } schema .referencers @@ -4842,26 +4673,14 @@ impl DirectiveDefinitionPosition { .directives .contains_key(&self.directive_name) { - return Err(SingleFederationError::Internal { - message: format!("Directive \"{}\" has not been pre-inserted", self), - } - .into()); + bail!(r#"Directive "{self}" has not been pre-inserted"#); } if schema .schema .directive_definitions .contains_key(&self.directive_name) { - // TODO: Allow built-in shadowing instead of ignoring them - if is_graphql_reserved_name(&self.directive_name) - || GRAPHQL_BUILTIN_DIRECTIVE_NAMES.contains(&self.directive_name) - { - return Ok(()); - } - return Err(SingleFederationError::Internal { - message: format!("Directive \"{}\" already exists in schema", self), - } - .into()); + bail!(r#"Directive "{self}" already exists in schema"#); } schema .schema @@ -5069,10 +4888,7 @@ impl DirectiveArgumentDefinitionPosition { referencers: &mut Referencers, ) -> Result<(), FederationError> { if is_graphql_reserved_name(&self.argument_name) { - return Err(SingleFederationError::Internal { - message: format!("Cannot insert reserved directive argument \"{}\"", self), - } - .into()); + bail!(r#"Cannot insert reserved directive argument "{self}""#); } validate_node_directives(argument.directives.deref())?; for directive_reference in argument.directives.iter() { @@ -5087,10 +4903,7 @@ impl DirectiveArgumentDefinitionPosition { referencers: &mut Referencers, ) -> Result<(), FederationError> { if is_graphql_reserved_name(&self.argument_name) { - return Err(SingleFederationError::Internal { - message: format!("Cannot remove reserved directive argument \"{}\"", self), - } - .into()); + bail!(r#"Cannot remove reserved directive argument "{self}""#); } for directive_reference in argument.directives.iter() { self.remove_directive_name_references(referencers, &directive_reference.name); @@ -5198,29 +5011,7 @@ pub(crate) fn is_graphql_reserved_name(name: &str) -> bool { name.starts_with("__") } -lazy_static! { - static ref GRAPHQL_BUILTIN_SCALAR_NAMES: IndexSet = { - IndexSet::from_iter([ - name!("Int"), - name!("Float"), - name!("String"), - name!("Boolean"), - name!("ID"), - ]) - }; - static ref GRAPHQL_BUILTIN_DIRECTIVE_NAMES: IndexSet = { - IndexSet::from_iter([ - name!("include"), - name!("skip"), - name!("deprecated"), - name!("specifiedBy"), - name!("defer"), - ]) - }; - // This is static so that UnionTypenameFieldDefinitionPosition.field_name() can return `&Name`, - // like the other field_name() methods in this file. - pub(crate) static ref INTROSPECTION_TYPENAME_FIELD_NAME: Name = name!("__typename"); -} +pub(crate) static INTROSPECTION_TYPENAME_FIELD_NAME: Name = name!("__typename"); fn validate_component_directives( directives: &[Component], diff --git a/apollo-federation/src/subgraph/spec.rs b/apollo-federation/src/subgraph/spec.rs index 44ba278309..4e8676e498 100644 --- a/apollo-federation/src/subgraph/spec.rs +++ b/apollo-federation/src/subgraph/spec.rs @@ -38,9 +38,11 @@ use crate::subgraph::spec::FederationSpecError::UnsupportedFederationDirective; use crate::subgraph::spec::FederationSpecError::UnsupportedVersionError; pub const COMPOSE_DIRECTIVE_NAME: Name = name!("composeDirective"); +pub const CONTEXT_DIRECTIVE_NAME: Name = name!("context"); pub const KEY_DIRECTIVE_NAME: Name = name!("key"); pub const EXTENDS_DIRECTIVE_NAME: Name = name!("extends"); pub const EXTERNAL_DIRECTIVE_NAME: Name = name!("external"); +pub const FROM_CONTEXT_DIRECTIVE_NAME: Name = name!("fromContext"); pub const INACCESSIBLE_DIRECTIVE_NAME: Name = name!("inaccessible"); pub const INTF_OBJECT_DIRECTIVE_NAME: Name = name!("interfaceObject"); pub const OVERRIDE_DIRECTIVE_NAME: Name = name!("override"); @@ -48,8 +50,6 @@ pub const PROVIDES_DIRECTIVE_NAME: Name = name!("provides"); pub const REQUIRES_DIRECTIVE_NAME: Name = name!("requires"); pub const SHAREABLE_DIRECTIVE_NAME: Name = name!("shareable"); pub const TAG_DIRECTIVE_NAME: Name = name!("tag"); -pub const CONTEXT_DIRECTIVE_NAME: Name = name!("context"); -pub const FROM_CONTEXT_DIRECTIVE_NAME: Name = name!("fromContext"); pub const FIELDSET_SCALAR_NAME: Name = name!("FieldSet"); // federated types @@ -68,11 +68,13 @@ pub const FEDERATION_V1_DIRECTIVE_NAMES: [Name; 5] = [ REQUIRES_DIRECTIVE_NAME, ]; -pub const FEDERATION_V2_DIRECTIVE_NAMES: [Name; 11] = [ +pub const FEDERATION_V2_DIRECTIVE_NAMES: [Name; 13] = [ COMPOSE_DIRECTIVE_NAME, + CONTEXT_DIRECTIVE_NAME, KEY_DIRECTIVE_NAME, EXTENDS_DIRECTIVE_NAME, EXTERNAL_DIRECTIVE_NAME, + FROM_CONTEXT_DIRECTIVE_NAME, INACCESSIBLE_DIRECTIVE_NAME, INTF_OBJECT_DIRECTIVE_NAME, OVERRIDE_DIRECTIVE_NAME, @@ -86,9 +88,11 @@ pub const FEDERATION_V2_DIRECTIVE_NAMES: [Name; 11] = [ // in FederationSpecDefinitions.directive_definition() for more information. enum FederationDirectiveName { Compose, + Context, Key, Extends, External, + FromContext, Inaccessible, IntfObject, Override, @@ -102,9 +106,14 @@ lazy_static! { static ref FEDERATION_DIRECTIVE_NAMES_TO_ENUM: IndexMap = { IndexMap::from_iter([ (COMPOSE_DIRECTIVE_NAME, FederationDirectiveName::Compose), + (CONTEXT_DIRECTIVE_NAME, FederationDirectiveName::Context), (KEY_DIRECTIVE_NAME, FederationDirectiveName::Key), (EXTENDS_DIRECTIVE_NAME, FederationDirectiveName::Extends), (EXTERNAL_DIRECTIVE_NAME, FederationDirectiveName::External), + ( + FROM_CONTEXT_DIRECTIVE_NAME, + FederationDirectiveName::FromContext, + ), ( INACCESSIBLE_DIRECTIVE_NAME, FederationDirectiveName::Inaccessible, @@ -285,9 +294,11 @@ impl FederationSpecDefinitions { }; Ok(match enum_name { FederationDirectiveName::Compose => self.compose_directive_definition(alias), + FederationDirectiveName::Context => self.context_directive_definition(alias), FederationDirectiveName::Key => self.key_directive_definition(alias)?, FederationDirectiveName::Extends => self.extends_directive_definition(alias), FederationDirectiveName::External => self.external_directive_definition(alias), + FederationDirectiveName::FromContext => self.from_context_directive_definition(alias), FederationDirectiveName::Inaccessible => self.inaccessible_directive_definition(alias), FederationDirectiveName::IntfObject => { self.interface_object_directive_definition(alias) @@ -339,6 +350,28 @@ impl FederationSpecDefinitions { } } + /// directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + fn context_directive_definition(&self, alias: &Option) -> DirectiveDefinition { + DirectiveDefinition { + description: None, + name: alias.clone().unwrap_or(CONTEXT_DIRECTIVE_NAME), + arguments: vec![InputValueDefinition { + description: None, + name: name!("name"), + ty: ty!(String!).into(), + default_value: None, + directives: Default::default(), + } + .into()], + repeatable: true, + locations: vec![ + DirectiveLocation::Interface, + DirectiveLocation::Object, + DirectiveLocation::Union, + ], + } + } + /// directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE fn key_directive_definition( &self, @@ -388,6 +421,29 @@ impl FederationSpecDefinitions { } } + // The directive is named `@fromContex`. This is confusing for clippy, as + // `from` is a conventional prefix used in conversion methods, which do not + // take `self` as an argument. This function does **not** perform + // conversion, but extracts `@fromContext` directive definition. + /// directive @fromContext(field: String!) on ARGUMENT_DEFINITION + #[allow(clippy::wrong_self_convention)] + fn from_context_directive_definition(&self, alias: &Option) -> DirectiveDefinition { + DirectiveDefinition { + description: None, + name: alias.clone().unwrap_or(FROM_CONTEXT_DIRECTIVE_NAME), + arguments: vec![InputValueDefinition { + description: None, + name: name!("field"), + ty: ty!(String!).into(), + default_value: None, + directives: Default::default(), + } + .into()], + repeatable: false, + locations: vec![DirectiveLocation::ArgumentDefinition], + } + } + /// directive @inaccessible on /// | ARGUMENT_DEFINITION /// | ENUM diff --git a/apollo-federation/src/supergraph/mod.rs b/apollo-federation/src/supergraph/mod.rs index bc8893f3d5..2ae9173201 100644 --- a/apollo-federation/src/supergraph/mod.rs +++ b/apollo-federation/src/supergraph/mod.rs @@ -47,10 +47,12 @@ pub use self::subgraph::ValidFederationSubgraphs; use crate::error::FederationError; use crate::error::MultipleFederationErrors; use crate::error::SingleFederationError; +use crate::link::context_spec_definition::CONTEXT_VERSIONS; use crate::link::cost_spec_definition::CostSpecDefinition; use crate::link::federation_spec_definition::get_federation_spec_definition_from_subgraph; use crate::link::federation_spec_definition::FederationSpecDefinition; use crate::link::federation_spec_definition::FEDERATION_VERSIONS; +use crate::link::join_spec_definition::ContextArgument; use crate::link::join_spec_definition::FieldDirectiveArguments; use crate::link::join_spec_definition::JoinSpecDefinition; use crate::link::join_spec_definition::TypeDirectiveArguments; @@ -300,6 +302,7 @@ fn extract_subgraphs_from_fed_2_supergraph( supergraph_schema, subgraphs, graph_enum_value_name_to_subgraph_name, + federation_spec_definitions, join_spec_definition, &union_types, )?; @@ -339,6 +342,7 @@ fn extract_subgraphs_from_fed_2_supergraph( .schema() .directive_definitions .values() + .filter(|directive| !directive.is_built_in()) .filter_map(|directive_definition| { let executable_locations = directive_definition .locations @@ -709,6 +713,16 @@ fn extract_object_type_content( message: "@join__implements should exist for a fed2 supergraph".to_owned(), })?; + let context = supergraph_schema + .metadata() + .and_then(|metadata| metadata.for_identity(&Identity::context_identity())) + .and_then(|context_link| CONTEXT_VERSIONS.find(&context_link.url.version)) + .and_then(|context_spec_def| { + context_spec_def + .context_directive_name_in_schema(supergraph_schema) + .ok() + .map(|name_in_schema| (context_spec_def, name_in_schema)) + }); for TypeInfo { name: type_name, subgraph_info, @@ -747,6 +761,17 @@ fn extract_object_type_content( )?; } + if let Some((_context_spec_def, name_in_supergraph)) = &context { + apply_context_to_type( + type_, + subgraphs, + graph_enum_value_name_to_subgraph_name, + federation_spec_definitions, + name_in_supergraph, + &CompositeTypeDefinitionPosition::Object(pos.clone()), + )?; + } + for graph_enum_value in subgraph_info.keys() { let subgraph = get_subgraph( subgraphs, @@ -890,10 +915,10 @@ fn extract_interface_type_content( subgraph_info, } in info.iter() { - let type_ = InterfaceTypeDefinitionPosition { + let pos = InterfaceTypeDefinitionPosition { type_name: (*type_name).clone(), - } - .get(supergraph_schema.schema())?; + }; + let type_ = pos.get(supergraph_schema.schema())?; fn get_pos( subgraph: &FederationSubgraph, subgraph_info: &IndexMap, @@ -973,6 +998,27 @@ fn extract_interface_type_content( } } + let context = supergraph_schema + .metadata() + .and_then(|metadata| metadata.for_identity(&Identity::context_identity())) + .and_then(|context_link| CONTEXT_VERSIONS.find(&context_link.url.version)) + .and_then(|context_spec_def| { + context_spec_def + .context_directive_name_in_schema(supergraph_schema) + .ok() + .map(|name_in_schema| (context_spec_def, name_in_schema)) + }); + if let Some((_context_spec_def, name_in_supergraph)) = &context { + apply_context_to_type( + type_, + subgraphs, + graph_enum_value_name_to_subgraph_name, + federation_spec_definitions, + name_in_supergraph, + &CompositeTypeDefinitionPosition::Interface(pos.clone()), + )?; + } + for (field_name, field) in type_.fields.iter() { let mut field_directive_applications = Vec::new(); for directive in field.directives.get_all(&field_directive_definition.name) { @@ -1066,6 +1112,7 @@ fn extract_union_type_content( supergraph_schema: &FederationSchema, subgraphs: &mut FederationSubgraphs, graph_enum_value_name_to_subgraph_name: &IndexMap>, + federation_spec_definitions: &IndexMap, join_spec_definition: &JoinSpecDefinition, info: &[TypeInfo], ) -> Result<(), FederationError> { @@ -1149,6 +1196,28 @@ fn extract_union_type_content( )?; } } + + let context = supergraph_schema + .metadata() + .and_then(|metadata| metadata.for_identity(&Identity::context_identity())) + .and_then(|context_link| CONTEXT_VERSIONS.find(&context_link.url.version)) + .and_then(|context_spec_def| { + context_spec_def + .context_directive_name_in_schema(supergraph_schema) + .ok() + .map(|name_in_schema| (context_spec_def, name_in_schema)) + }); + + if let Some((_context_spec_def, name_in_supergraph)) = &context { + apply_context_to_type( + type_, + subgraphs, + graph_enum_value_name_to_subgraph_name, + federation_spec_definitions, + name_in_supergraph, + &CompositeTypeDefinitionPosition::Union(pos.clone()), + )?; + } } Ok(()) @@ -1389,6 +1458,7 @@ fn add_subgraph_field( override_: None, override_label: None, user_overridden: None, + context_arguments: None, }); let subgraph_field_type = match &field_directive_application.type_ { Some(t) => decode_type(t)?, @@ -1474,6 +1544,40 @@ fn add_subgraph_field( )?; } + if let Some(context_arguments) = &field_directive_application.context_arguments { + for args in context_arguments { + let ContextArgument { + name, + type_, + context, + selection, + } = args; + let (_, context_name_in_subgraph) = context.rsplit_once("__").ok_or_else(|| { + SingleFederationError::InvalidFederationSupergraph { + message: format!( + "Could not parse context field from supergraph '{}'", + context + ) + .to_owned(), + } + })?; + + let arg = format!("${} {}", context_name_in_subgraph, selection); + let from_context_directive = + federation_spec_definition.from_context_directive(&subgraph.schema, arg)?; + let directives = std::iter::once(from_context_directive).collect(); + let ty = decode_type(type_)?; + let node = Node::new(InputValueDefinition { + name: Name::new(name)?, + ty: ty.into(), + directives, + default_value: None, + description: None, + }); + subgraph_field.arguments.push(node); + } + } + match object_or_interface_field_definition_position { ObjectOrInterfaceFieldDefinitionPosition::Object(pos) => { pos.insert(&mut subgraph.schema, Component::from(subgraph_field))?; @@ -1504,6 +1608,7 @@ fn add_subgraph_input_field( override_: None, override_label: None, user_overridden: None, + context_arguments: None, }); let subgraph_input_field_type = match &field_directive_application.type_ { Some(t) => Node::new(decode_type(t)?), @@ -1561,6 +1666,16 @@ fn get_subgraph<'subgraph>( }) } +fn get_index_from_subgraph_name<'a>( + graph_enum_value_name_to_subgraph_name: &'a IndexMap>, + subgraph_name: &'a Name, +) -> Option<&'a Name> { + graph_enum_value_name_to_subgraph_name + .iter() + .find(|(_, v)| v.as_ref() == subgraph_name.as_str()) + .map(|(k, _)| k) +} + lazy_static! { static ref EXECUTABLE_DIRECTIVE_LOCATIONS: IndexSet = { [ @@ -1578,6 +1693,103 @@ lazy_static! { }; } +fn insert_directive( + schema: &mut FederationSchema, + pos: &CompositeTypeDefinitionPosition, + directive: Component, +) -> Result<(), FederationError> { + match pos { + CompositeTypeDefinitionPosition::Union(pos) => pos.insert_directive(schema, directive), + CompositeTypeDefinitionPosition::Object(pos) => pos.insert_directive(schema, directive), + CompositeTypeDefinitionPosition::Interface(pos) => pos.insert_directive(schema, directive), + } +} + +trait CompositeType { + fn directives(&self) -> &DirectiveList; +} + +impl CompositeType for UnionType { + fn directives(&self) -> &DirectiveList { + &self.directives + } +} + +impl CompositeType for ObjectType { + fn directives(&self) -> &DirectiveList { + &self.directives + } +} + +impl CompositeType for InterfaceType { + fn directives(&self) -> &DirectiveList { + &self.directives + } +} + +fn apply_context_to_type( + ty: &Node, + subgraphs: &mut FederationSubgraphs, + graph_enum_value_name_to_subgraph_name: &IndexMap>, + federation_spec_definitions: &IndexMap, + context_name_in_supergraph: &Name, + pos: &CompositeTypeDefinitionPosition, +) -> Result<(), FederationError> +where + T: CompositeType, +{ + for directive in ty.directives().get_all(context_name_in_supergraph.as_str()) { + FederationSpecDefinition::context_directive_arguments(directive).and_then( + |context_name| { + let mut arr = context_name.name.split("__"); + let subgraph_name = arr.next().ok_or_else(|| { + SingleFederationError::InvalidFederationSupergraph { + message: format!( + "Could not parse context name from supergraph '{}'", + context_name_in_supergraph + ) + .to_owned(), + } + })?; + let subgraph_name = Name::new_unchecked(subgraph_name); + let subgraph_index = get_index_from_subgraph_name( + graph_enum_value_name_to_subgraph_name, + &subgraph_name, + ) + .ok_or_else(|| SingleFederationError::InvalidSubgraph { + message: format!("Could not look up subgraph by name '{}'", subgraph_name) + .to_owned(), + })?; + let subgraph = get_subgraph( + subgraphs, + graph_enum_value_name_to_subgraph_name, + subgraph_index, + )?; + + let federation_spec_definition = federation_spec_definitions + .get(subgraph_index) + .ok_or_else(|| SingleFederationError::InvalidFederationSupergraph { + message: "Subgraph unexpectedly does not use federation spec".to_owned(), + })?; + let context_in_subgraph = arr.last().ok_or_else(|| { + SingleFederationError::InvalidFederationSupergraph { + message: format!( + "Could not parse context name from supergraph '{}'", + context_name_in_supergraph + ) + .to_owned(), + } + })?; + let context_directive = federation_spec_definition + .context_directive(&subgraph.schema, context_in_subgraph.to_string())?; + insert_directive(&mut subgraph.schema, pos, context_directive.into())?; + Ok(()) + }, + )?; + } + Ok(()) +} + fn remove_unused_types_from_subgraph(schema: &mut FederationSchema) -> Result<(), FederationError> { // We now do an additional path on all types because we sometimes added types to subgraphs // without being sure that the subgraph had the type in the first place (especially with the diff --git a/apollo-federation/src/supergraph/schema.rs b/apollo-federation/src/supergraph/schema.rs index 589131f633..82154f0d2e 100644 --- a/apollo-federation/src/supergraph/schema.rs +++ b/apollo-federation/src/supergraph/schema.rs @@ -99,6 +99,10 @@ pub(crate) fn new_empty_fed_2_subgraph_schema() -> Result(&mut self, mut init: O, mut mapper: F) -> Result + where + Self: Iterator, + F: FnMut(O, T) -> Result, + { + for item in self { + init = mapper(init, item)?; + } + Ok(init) + } + + fn and_then_fold(&mut self, mut init: O, mut mapper: F) -> Result + where + Self: Iterator>, + F: FnMut(O, T) -> Result, + { + for item in self { + init = mapper(init, item?)? + } + Ok(init) + } } impl FallibleIterator for I {} diff --git a/apollo-federation/src/utils/logging.rs b/apollo-federation/src/utils/logging.rs index c7a07c2ef2..0b247c1698 100644 --- a/apollo-federation/src/utils/logging.rs +++ b/apollo-federation/src/utils/logging.rs @@ -1,3 +1,10 @@ +#![allow(dead_code)] + +use crate::operation::Selection; +use crate::query_graph::graph_path::ClosedBranch; +use crate::query_graph::graph_path::SimultaneousPathsWithLazyIndirectPaths; +use crate::query_plan::query_planning_traversal::OpenBranchAndSelections; + /// This macro is a wrapper around `tracing::trace!` and should not be confused with our snapshot /// testing. This primary goal of this macro is to add the necessary context to logging statements /// so that external tools (like the snapshot log visualizer) can show how various key data @@ -32,17 +39,6 @@ macro_rules! snapshot { $msg ); }; - (name = $name:literal, $value:expr, $msg:literal) => { - #[cfg(feature = "snapshot_tracing")] - tracing::trace!( - snapshot = std::any::type_name_of_val(&$value), - data = ron::ser::to_string(&$value).expect(concat!( - "Could not serialize value for a snapshot with message: ", - $msg - )), - $msg - ); - }; ($name:literal, $value:expr, $msg:literal) => { #[cfg(feature = "snapshot_tracing")] tracing::trace!(snapshot = $name, data = $value, $msg); @@ -50,3 +46,76 @@ macro_rules! snapshot { } pub(crate) use snapshot; + +pub(crate) fn make_string( + data: &T, + writer: fn(&mut std::fmt::Formatter<'_>, &T) -> std::fmt::Result, +) -> String { + // One-off struct to implement `Display` for `data` using `writer`. + struct Stringify<'a, T: ?Sized> { + data: &'a T, + writer: fn(&mut std::fmt::Formatter<'_>, &T) -> std::fmt::Result, + } + + impl<'a, T: ?Sized> std::fmt::Display for Stringify<'a, T> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + (self.writer)(f, self.data) + } + } + + Stringify { data, writer }.to_string() +} + +// PORT_NOTE: This is a (partial) port of `QueryPlanningTraversal.debugStack` JS method. +pub(crate) fn format_open_branch( + f: &mut std::fmt::Formatter<'_>, + (selection, options): &(&Selection, &[SimultaneousPathsWithLazyIndirectPaths]), +) -> std::fmt::Result { + writeln!(f, "{selection}")?; + writeln!(f, " * Options:")?; + for option in *options { + writeln!(f, " - {option}")?; + } + Ok(()) +} + +pub(crate) fn open_branch_to_string( + selection: &Selection, + options: &[SimultaneousPathsWithLazyIndirectPaths], +) -> String { + make_string(&(selection, options), format_open_branch) +} + +// PORT_NOTE: This is a port of `QueryPlanningTraversal.debugStack` JS method. +pub(crate) fn format_open_branches( + f: &mut std::fmt::Formatter<'_>, + open_branches: &[OpenBranchAndSelections], +) -> std::fmt::Result { + // Print from the stack top to the bottom. + for branch in open_branches.iter().rev() { + writeln!(f, "{branch}")?; + } + Ok(()) +} + +pub(crate) fn open_branches_to_string(open_branches: &[OpenBranchAndSelections]) -> String { + make_string(open_branches, format_open_branches) +} + +pub(crate) fn format_closed_branches( + f: &mut std::fmt::Formatter<'_>, + closed_branches: &[ClosedBranch], +) -> std::fmt::Result { + writeln!(f, "All branches:")?; + for (i, closed_branch) in closed_branches.iter().enumerate() { + writeln!(f, "{i}:")?; + for closed_path in &closed_branch.0 { + writeln!(f, " - {closed_path}")?; + } + } + Ok(()) +} + +pub(crate) fn closed_branches_to_string(closed_branches: &[ClosedBranch]) -> String { + make_string(closed_branches, format_closed_branches) +} diff --git a/apollo-federation/tests/extract_subgraphs.rs b/apollo-federation/tests/extract_subgraphs.rs index 2148185184..9fbde4e473 100644 --- a/apollo-federation/tests/extract_subgraphs.rs +++ b/apollo-federation/tests/extract_subgraphs.rs @@ -697,3 +697,111 @@ fn does_not_extract_renamed_demand_control_directive_name_conflicts() { } insta::assert_snapshot!(snapshot); } + +#[test] +fn extracts_set_context_directives() { + let subgraphs = Supergraph::new(r#" + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1") + { + query: Query + } + + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + + directive @join__graph(name: String!, url: String!) on ENUM_VALUE + + directive @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__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + + directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + + directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + + directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + + directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + + directive @context__fromContext(field: String) on ARGUMENT_DEFINITION + + enum link__Purpose { + """ + \`SECURITY\` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + \`EXECUTION\` features provide metadata necessary for operation execution. + """ + EXECUTION + } + + scalar link__Import + + enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "") + SUBGRAPH2 @join__graph(name: "Subgraph2", url: "") + } + + scalar join__FieldSet + + scalar join__DirectiveArguments + + scalar join__FieldValue + + input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! + } + + scalar context__context + + type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) + { + t: T! @join__field(graph: SUBGRAPH1) + a: Int! @join__field(graph: SUBGRAPH2) + } + + type T + @join__type(graph: SUBGRAPH1, key: "id") + @context(name: "Subgraph1__context") + { + id: ID! + u: U! + prop: String! + } + + type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") + { + id: ID! + field: Int! @join__field(graph: SUBGRAPH1, contextArguments: [{context: "Subgraph1__context", name: "a", type: "String", selection: "{ prop }"}]) + } + "#) + .expect("is supergraph") + .extract_subgraphs() + .expect("extracts subgraphs"); + + let mut snapshot = String::new(); + for (_name, subgraph) in subgraphs { + use std::fmt::Write; + + _ = writeln!( + &mut snapshot, + "{}\n---\n{}", + subgraph.name, + subgraph.schema.schema() + ); + } + insta::assert_snapshot!(snapshot); +} 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 b73596590b..b7e02865c6 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_support.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_support.rs @@ -12,9 +12,9 @@ use apollo_federation::query_plan::TopLevelPlanNode; use apollo_federation::schema::ValidFederationSchema; use sha1::Digest; -const ROVER_FEDERATION_VERSION: &str = "2.7.4"; +const ROVER_FEDERATION_VERSION: &str = "2.9.0"; -const DEFAULT_LINK_DIRECTIVE: &str = r#"@link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])"#; +const DEFAULT_LINK_DIRECTIVE: &str = r#"@link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject", "@context", "@fromContext", "@cost", "@listSize"])"#; /// Runs composition on the given subgraph schemas and return `(api_schema, query_planner)` /// 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 0e93f5a229..c0d85c7b62 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,7 @@ fn some_name() { } */ +mod context; mod debug_max_evaluated_plans_configuration; mod defer; mod fetch_operation_names; @@ -1361,3 +1362,53 @@ fn condition_order_router799() { "### ); } + +#[test] +fn rebase_non_intersecting_without_dropping_inline_fragment_due_to_directive() { + let planner = planner!( + Subgraph1: r#" + type Query { + test: X + } + + interface I { + i: Int + } + + type X implements I { + i: Int + } + + type Y implements I { + i: Int + } + "#, + ); + assert_plan!( + &planner, + r#" + { + test { # type X + ... on I { # upcast to I + ... @skip(if: false) { # This fragment can't be dropped due to its directive. + ... on Y { # downcast to Y (non-intersecting) + i + } + } + } + } + } + "#, + @r###" + QueryPlan { + Fetch(service: "Subgraph1") { + { + test { + __typename @include(if: false) + } + } + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/context.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/context.rs new file mode 100644 index 0000000000..4bdf9f41b1 --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/context.rs @@ -0,0 +1,1491 @@ +// PORT_NOTE: The context tests in the JS code had more involved setup compared to the other tests. +// Here is a snippet from the JS context test leading up to the creation of the planner: +// ```js +// const asFed2Service = (service: ServiceDefinition) => { +// return { +// ...service, +// typeDefs: asFed2SubgraphDocument(service.typeDefs, { +// includeAllImports: true, +// }), +// }; +// }; +// +// const composeAsFed2Subgraphs = (services: ServiceDefinition[]) => { +// return composeServices(services.map((s) => asFed2Service(s))); +// }; +// +// const result = composeAsFed2Subgraphs([subgraph1, subgraph2]); +// const [api, queryPlanner] = [ +// result.schema!.toAPISchema(), +// new QueryPlanner(Supergraph.buildForTests(result.supergraphSdl!)), +// ]; +// ``` +// For all other tests, the set up was a single line: +// ```js +// const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); +// ``` +// +// How this needs to be ported remains to be seen... + +use std::sync::Arc; + +use apollo_compiler::Name; +use apollo_federation::query_plan::FetchDataKeyRenamer; +use apollo_federation::query_plan::FetchDataPathElement; +use apollo_federation::query_plan::FetchDataRewrite; +use apollo_federation::query_plan::PlanNode; +use apollo_federation::query_plan::TopLevelPlanNode; + +#[test] +fn set_context_test_variable_is_from_same_subgraph() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T! + } + type T @key(fields: "id") @context(name: "context") { + id: ID! + u: U! + prop: String! + } + type U @key(fields: "id") { + id: ID! + b: String! + field(a: String @fromContext(field: "$context { prop }")): Int! + } + "#, + Subgraph2: r#" + type Query { + a: Int! + } + type U @key(fields: "id") { + id: ID! + } + "#, + ); + + let plan = assert_plan!(planner, + r#" + { + t { + u { + b + field + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + prop + u { + __typename + id + b + } + } + } + }, + Flatten(path: "t.u") { + Fetch(service: "Subgraph1") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + field(a: $contextualArgument_1_0) + } + } + }, + }, + }, + } + "### + ); + match plan.node { + Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { + Some(PlanNode::Flatten(node)) => match &*node.node { + PlanNode::Fetch(node) => { + assert_eq!( + node.context_rewrites, + vec![Arc::new(FetchDataRewrite::KeyRenamer( + FetchDataKeyRenamer { + rename_key_to: Name::new("contextualArgument_1_0").unwrap(), + path: vec![ + FetchDataPathElement::Parent, + FetchDataPathElement::TypenameEquals(Name::new("T").unwrap()), + FetchDataPathElement::Key( + Name::new("prop").unwrap(), + Default::default() + ), + ], + } + )),] + ); + } + _ => panic!("failed to get fetch node"), + }, + _ => panic!("failed to get flatten node"), + }, + _ => panic!("failed to get sequence node"), + } +} + +#[test] +fn set_context_test_variable_is_from_different_subgraph() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T! + } + type T @key(fields: "id") @context(name: "context") { + id: ID! + u: U! + prop: String! @external + } + type U @key(fields: "id") { + id: ID! + field(a: String @fromContext(field: "$context { prop }")): Int! + } + "#, + Subgraph2: r#" + type Query { + a: Int! + } + type T @key(fields: "id") { + id: ID! + prop: String! + } + type U @key(fields: "id") { + id: ID! + } + "#, + ); + let plan = assert_plan!( + planner, + r#" + { + t { + u { + id + field + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + id + u { + __typename + id + } + } + } + }, + Flatten(path: "t") { + Fetch(service: "Subgraph2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + prop + } + } + }, + }, + Flatten(path: "t.u") { + Fetch(service: "Subgraph1") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + field(a: $contextualArgument_1_0) + } + } + }, + }, + }, + } + "###); + + match plan.node { + Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(2) { + Some(PlanNode::Flatten(node)) => match &*node.node { + PlanNode::Fetch(node) => { + assert_eq!( + node.context_rewrites, + vec![Arc::new(FetchDataRewrite::KeyRenamer( + FetchDataKeyRenamer { + rename_key_to: Name::new("contextualArgument_1_0").unwrap(), + path: vec![ + FetchDataPathElement::Parent, + FetchDataPathElement::TypenameEquals(Name::new("T").unwrap()), + FetchDataPathElement::Key( + Name::new("prop").unwrap(), + Default::default() + ), + ], + } + )),] + ); + } + _ => panic!("failed to get fetch node"), + }, + _ => panic!("failed to get flatten node"), + }, + _ => panic!("failed to get sequence node"), + } +} + +#[test] +fn set_context_test_variable_is_already_in_a_different_fetch_group() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T! + } + type T @key(fields: "id") { + id: ID! + u: U! + prop: String! + } + type U @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type Query { + a: Int! + } + + type T @key(fields: "id") @context(name: "context") { + id: ID! + prop: String! @external + } + + type U @key(fields: "id") { + id: ID! + field(a: String @fromContext(field: "$context { prop }")): Int! + } + "#, + ); + + let plan = assert_plan!(planner, + r#" + { + t { + u { + id + field + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + prop + u { + __typename + id + } + } + } + }, + Flatten(path: "t.u") { + Fetch(service: "Subgraph2") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + field(a: $contextualArgument_1_0) + } + } + }, + }, + }, + } + "### + ); + match plan.node { + Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { + Some(PlanNode::Flatten(node)) => match &*node.node { + PlanNode::Fetch(node) => { + assert_eq!( + node.context_rewrites, + vec![Arc::new(FetchDataRewrite::KeyRenamer( + FetchDataKeyRenamer { + rename_key_to: Name::new("contextualArgument_1_0").unwrap(), + path: vec![ + FetchDataPathElement::Parent, + FetchDataPathElement::TypenameEquals(Name::new("T").unwrap()), + FetchDataPathElement::Key( + Name::new("prop").unwrap(), + Default::default() + ), + ], + } + )),] + ); + } + _ => panic!("failed to get fetch node"), + }, + _ => panic!("failed to get flatten node"), + }, + _ => panic!("failed to get sequence node"), + } +} + +#[test] +fn set_context_test_variable_is_a_list() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T! + } + type T @key(fields: "id") @context(name: "context") { + id: ID! + u: U! + prop: [String]! + } + type U @key(fields: "id") { + id: ID! + field(a: [String] @fromContext(field: "$context { prop }")): Int! + } + "#, + Subgraph2: r#" + type Query { + a: Int! + } + type U @key(fields: "id") { + id: ID! + } + "# + ); + + let plan = assert_plan!(planner, + r#" + { + t { + u { + field + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + prop + u { + __typename + id + } + } + } + }, + Flatten(path: "t.u") { + Fetch(service: "Subgraph1") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + field(a: $contextualArgument_1_0) + } + } + }, + }, + }, + } + "### + ); + match plan.node { + Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { + Some(PlanNode::Flatten(node)) => match &*node.node { + PlanNode::Fetch(node) => { + assert_eq!( + node.context_rewrites, + vec![Arc::new(FetchDataRewrite::KeyRenamer( + FetchDataKeyRenamer { + rename_key_to: Name::new("contextualArgument_1_0").unwrap(), + path: vec![ + FetchDataPathElement::Parent, + FetchDataPathElement::TypenameEquals(Name::new("T").unwrap()), + FetchDataPathElement::Key( + Name::new("prop").unwrap(), + Default::default() + ), + ], + } + )),] + ); + } + _ => panic!("failed to get fetch node"), + }, + _ => panic!("failed to get flatten node"), + }, + _ => panic!("failed to get sequence node"), + } +} + +#[test] +fn set_context_test_fetched_as_a_list() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: [T]! + } + type T @key(fields: "id") @context(name: "context") { + id: ID! + u: U! + prop: String! + } + type U @key(fields: "id") { + id: ID! + b: String! + field(a: String @fromContext(field: "$context { prop }")): Int! + } + "#, + Subgraph2: r#" + type Query { + a: Int! + } + type U @key(fields: "id") { + id: ID! + } + "#, + ); + + let plan = assert_plan!(planner, + r#" + { + t { + u { + b + field + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + prop + u { + __typename + id + b + } + } + } + }, + Flatten(path: "t.@.u") { + Fetch(service: "Subgraph1") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + field(a: $contextualArgument_1_0) + } + } + }, + }, + }, + } + "### + ); + match plan.node { + Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { + Some(PlanNode::Flatten(node)) => match &*node.node { + PlanNode::Fetch(node) => { + assert_eq!( + node.context_rewrites, + vec![Arc::new(FetchDataRewrite::KeyRenamer( + FetchDataKeyRenamer { + rename_key_to: Name::new("contextualArgument_1_0").unwrap(), + path: vec![ + FetchDataPathElement::Parent, + FetchDataPathElement::TypenameEquals(Name::new("T").unwrap()), + FetchDataPathElement::Key( + Name::new("prop").unwrap(), + Default::default() + ), + ], + } + )),] + ); + } + _ => panic!("failed to get fetch node"), + }, + _ => panic!("failed to get flatten node"), + }, + _ => panic!("failed to get sequence node"), + } +} + +#[test] +fn set_context_test_impacts_on_query_planning() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: I! + } + + interface I @context(name: "context") @key(fields: "id") { + id: ID! + u: U! + prop: String! + } + + type A implements I @key(fields: "id") { + id: ID! + u: U! + prop: String! + } + + type B implements I @key(fields: "id") { + id: ID! + u: U! + prop: String! + } + + type U @key(fields: "id") { + id: ID! + b: String! + field(a: String @fromContext(field: "$context { prop }")): Int! + } + "#, + Subgraph2: r#" + type Query { + a: Int! + } + type U @key(fields: "id") { + id: ID! + } + "#, + ); + + let plan = assert_plan!(planner, + r#" + { + t { + u { + b + field + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + prop + u { + __typename + id + b + } + } + } + }, + Flatten(path: "t.u") { + Fetch(service: "Subgraph1") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + field(a: $contextualArgument_1_0) + } + } + }, + }, + }, + } + "### + ); + match plan.node { + Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { + Some(PlanNode::Flatten(node)) => match &*node.node { + PlanNode::Fetch(node) => { + assert_eq!( + node.context_rewrites, + vec![ + Arc::new(FetchDataRewrite::KeyRenamer(FetchDataKeyRenamer { + rename_key_to: Name::new("contextualArgument_1_0").unwrap(), + path: vec![ + FetchDataPathElement::Parent, + FetchDataPathElement::TypenameEquals(Name::new("A").unwrap()), + FetchDataPathElement::Key( + Name::new("prop").unwrap(), + Default::default() + ), + ], + })), + Arc::new(FetchDataRewrite::KeyRenamer(FetchDataKeyRenamer { + rename_key_to: Name::new("contextualArgument_1_0").unwrap(), + path: vec![ + FetchDataPathElement::Parent, + FetchDataPathElement::TypenameEquals(Name::new("B").unwrap()), + FetchDataPathElement::Key( + Name::new("prop").unwrap(), + Default::default() + ), + ], + })), + ] + ); + } + _ => panic!("failed to get fetch node"), + }, + _ => panic!("failed to get flatten node"), + }, + _ => panic!("failed to get sequence node"), + } +} + +#[test] +fn set_context_test_with_type_conditions_for_union() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T! + } + + union T @context(name: "context") = A | B + + type A @key(fields: "id") { + id: ID! + u: U! + prop: String! + } + + type B @key(fields: "id") { + id: ID! + u: U! + prop: String! + } + + type U @key(fields: "id") { + id: ID! + b: String! + field( + a: String + @fromContext( + field: "$context ... on A { prop } ... on B { prop }" + ) + ): Int! + } + "#, + Subgraph2: r#" + type Query { + a: Int! + } + type U @key(fields: "id") { + id: ID! + } + "#, + ); + + let plan = assert_plan!(planner, + r#" + { + t { + ... on A { + u { + b + field + } + } + ... on B { + u { + b + field + } + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + ... on A { + __typename + prop + u { + __typename + id + b + } + } + ... on B { + __typename + prop + u { + __typename + id + b + } + } + } + } + }, + Flatten(path: "t.u") { + Fetch(service: "Subgraph1") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + field(a: $contextualArgument_1_0) + } + } + }, + }, + }, + } + "### + ); + match plan.node { + Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { + Some(PlanNode::Flatten(node)) => match &*node.node { + PlanNode::Fetch(node) => { + assert_eq!( + node.context_rewrites, + vec![ + Arc::new(FetchDataRewrite::KeyRenamer(FetchDataKeyRenamer { + rename_key_to: Name::new("contextualArgument_1_0").unwrap(), + path: vec![ + FetchDataPathElement::Parent, + FetchDataPathElement::TypenameEquals(Name::new("A").unwrap()), + FetchDataPathElement::Key( + Name::new("prop").unwrap(), + Default::default() + ), + ], + })), + Arc::new(FetchDataRewrite::KeyRenamer(FetchDataKeyRenamer { + rename_key_to: Name::new("contextualArgument_1_0").unwrap(), + path: vec![ + FetchDataPathElement::Parent, + FetchDataPathElement::TypenameEquals(Name::new("B").unwrap()), + FetchDataPathElement::Key( + Name::new("prop").unwrap(), + Default::default() + ), + ], + })), + ] + ); + } + _ => panic!("failed to get fetch node"), + }, + _ => panic!("failed to get flatten node"), + }, + _ => panic!("failed to get sequence node"), + } +} + +#[test] +fn set_context_test_accesses_a_different_top_level_query() { + let planner = planner!( + Subgraph1: r#" + type Query @context(name: "topLevelQuery") { + me: User! + product: Product + } + + type User @key(fields: "id") { + id: ID! + locale: String! + } + + type Product @key(fields: "id") { + id: ID! + price( + locale: String + @fromContext(field: "$topLevelQuery { me { locale } }") + ): Int! + } + "#, + Subgraph2: r#" + type Query { + randomId: ID! + } + + type Product @key(fields: "id") { + id: ID! + } + "#, + ); + + let plan = assert_plan!(planner, + r#" + { + product { + price + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + __typename + me { + locale + } + product { + __typename + id + } + } + }, + Flatten(path: "product") { + Fetch(service: "Subgraph1") { + { + ... on Product { + __typename + id + } + } => + { + ... on Product { + price(locale: $contextualArgument_1_0) + } + } + }, + }, + }, + } + "### + ); + match plan.node { + Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { + Some(PlanNode::Flatten(node)) => match &*node.node { + PlanNode::Fetch(node) => { + assert_eq!( + node.context_rewrites, + vec![Arc::new(FetchDataRewrite::KeyRenamer( + FetchDataKeyRenamer { + rename_key_to: Name::new("contextualArgument_1_0").unwrap(), + path: vec![ + FetchDataPathElement::Parent, + FetchDataPathElement::Key( + Name::new("me").unwrap(), + Default::default() + ), + FetchDataPathElement::Key( + Name::new("locale").unwrap(), + Default::default() + ), + ], + } + )),] + ); + } + _ => panic!("failed to get fetch node"), + }, + _ => panic!("failed to get flatten node"), + }, + _ => panic!("failed to get sequence node"), + } +} + +#[test] +fn set_context_one_subgraph() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T! + } + type T @key(fields: "id") @context(name: "context") { + id: ID! + u: U! + prop: String! + } + type U @key(fields: "id") { + id: ID! + b: String! + field(a: String @fromContext(field: "$context { prop }")): Int! + } + "#, + Subgraph2: r#" + type Query { + randomId: ID! + } + "#, + ); + + let plan = assert_plan!(planner, + r#" + { + t { + u { + field + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + prop + u { + __typename + id + } + } + } + }, + Flatten(path: "t.u") { + Fetch(service: "Subgraph1") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + field(a: $contextualArgument_1_0) + } + } + }, + }, + }, + } + "### + ); + match plan.node { + Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { + Some(PlanNode::Flatten(node)) => match &*node.node { + PlanNode::Fetch(node) => { + assert_eq!( + node.context_rewrites, + vec![Arc::new(FetchDataRewrite::KeyRenamer( + FetchDataKeyRenamer { + rename_key_to: Name::new("contextualArgument_1_0").unwrap(), + path: vec![ + FetchDataPathElement::Parent, + FetchDataPathElement::TypenameEquals(Name::new("T").unwrap()), + FetchDataPathElement::Key( + Name::new("prop").unwrap(), + Default::default() + ), + ], + } + )),] + ); + } + _ => panic!("failed to get fetch node"), + }, + _ => panic!("failed to get flatten node"), + }, + _ => panic!("failed to get sequence node"), + } +} + +#[test] +fn set_context_required_field_is_several_levels_deep_going_back_and_forth_between_subgraphs() { + let planner = planner!( + Subgraph1: r#" + type Query { + t: T! + } + + type A @key(fields: "id") { + id: ID! + b: B! @external + } + + type B @key(fields: "id") { + id: ID! + c: C! + } + + type C @key(fields: "id") { + id: ID! + prop: String! + } + + type T @key(fields: "id") @context(name: "context") { + id: ID! + u: U! + a: A! + } + type U @key(fields: "id") { + id: ID! + b: String! + field( + a: String @fromContext(field: "$context { a { b { c { prop }}} }") + ): Int! + } + "#, + Subgraph2: r#" + type Query { + randomId: ID! + } + + type A @key(fields: "id") { + id: ID! + b: B! + } + + type B @key(fields: "id") { + id: ID! + } + "#, + ); + + let plan = assert_plan!(planner, + r#" + { + t { + u { + field + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + t { + __typename + a { + __typename + id + } + u { + __typename + id + } + } + } + }, + Flatten(path: "t.a") { + Fetch(service: "Subgraph2") { + { + ... on A { + __typename + id + } + } => + { + ... on A { + b { + __typename + id + } + } + } + }, + }, + Flatten(path: "t.a.b") { + Fetch(service: "Subgraph1") { + { + ... on B { + __typename + id + } + } => + { + ... on B { + c { + prop + } + } + } + }, + }, + Flatten(path: "t.u") { + Fetch(service: "Subgraph1") { + { + ... on U { + __typename + id + } + } => + { + ... on U { + field(a: $contextualArgument_1_0) + } + } + }, + }, + }, + } + "### + ); + match plan.node { + Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(3) { + Some(PlanNode::Flatten(node)) => match &*node.node { + PlanNode::Fetch(node) => { + assert_eq!( + node.context_rewrites, + vec![Arc::new(FetchDataRewrite::KeyRenamer( + FetchDataKeyRenamer { + rename_key_to: Name::new("contextualArgument_1_0").unwrap(), + path: vec![ + FetchDataPathElement::Parent, + FetchDataPathElement::TypenameEquals(Name::new("T").unwrap()), + FetchDataPathElement::Key( + Name::new("a").unwrap(), + Default::default() + ), + FetchDataPathElement::Key( + Name::new("b").unwrap(), + Default::default() + ), + FetchDataPathElement::Key( + Name::new("c").unwrap(), + Default::default() + ), + FetchDataPathElement::Key( + Name::new("prop").unwrap(), + Default::default() + ), + ], + } + )),] + ); + } + _ => panic!("failed to get fetch node"), + }, + _ => panic!("failed to get flatten node"), + }, + _ => panic!("failed to get sequence node"), + } +} + +#[test] +fn set_context_test_before_key_resolution_transition() { + let planner = planner!( + Subgraph1: r#" + type Query { + customer: Customer! + } + + type Identifiers @key(fields: "id") { + id: ID! + legacyUserId: ID! + } + + type Customer @key(fields: "id") { + id: ID! + child: Child! + identifiers: Identifiers! + } + + type Child @key(fields: "id") { + id: ID! + } + "#, + Subgraph2: r#" + type Customer @key(fields: "id") @context(name: "ctx") { + id: ID! + identifiers: Identifiers! @external + } + + type Identifiers @key(fields: "id") { + id: ID! + legacyUserId: ID! @external + } + + type Child @key(fields: "id") { + id: ID! + prop( + legacyUserId: ID + @fromContext(field: "$ctx { identifiers { legacyUserId } }") + ): String + } + "#, + ); + + assert_plan!(planner, + r#" + query { + customer { + child { + id + prop + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph1") { + { + customer { + __typename + identifiers { + legacyUserId + } + child { + __typename + id + } + } + } + }, + Flatten(path: "customer.child") { + Fetch(service: "Subgraph2") { + { + ... on Child { + __typename + id + } + } => + { + ... on Child { + prop(legacyUserId: $contextualArgument_1_0) + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn set_context_test_efficiently_merge_fetch_groups() { + let planner = planner!( + Subgraph1: r#" + type Identifiers @key(fields: "id") { + id: ID! + id2: ID @external + id3: ID @external + wid: ID @requires(fields: "id2 id3") + } + "#, + Subgraph2: r#" + type Query { + customer: Customer + } + + type Customer @key(fields: "id") { + id: ID! + identifiers: Identifiers + mid: ID + } + + type Identifiers @key(fields: "id") { + id: ID! + id2: ID + id3: ID + id5: ID + } + "#, + Subgraph3: r#" + type Customer @key(fields: "id") @context(name: "retailCtx") { + accounts: Accounts @shareable + id: ID! + mid: ID @external + identifiers: Identifiers @external + } + + type Identifiers @key(fields: "id") { + id: ID! + id5: ID @external + } + type Accounts @key(fields: "id") { + foo( + randomInput: String + ctx_id5: ID + @fromContext(field: "$retailCtx { identifiers { id5 } }") + ctx_mid: ID @fromContext(field: "$retailCtx { mid }") + ): Foo + id: ID! + } + + type Foo { + id: ID + } + "#, + Subgraph4: r#" + type Customer + @key(fields: "id", resolvable: false) + @context(name: "widCtx") { + accounts: Accounts @shareable + id: ID! + identifiers: Identifiers @external + } + + type Identifiers @key(fields: "id", resolvable: false) { + id: ID! + wid: ID @external # @requires(fields: "id2 id3") + } + + type Accounts @key(fields: "id") { + bar( + ctx_wid: ID @fromContext(field: "$widCtx { identifiers { wid } }") + ): Bar + + id: ID! + } + + type Bar { + id: ID + } + "#, + ); + + let plan = assert_plan!(planner, + r#" + query { + customer { + accounts { + foo { + id + } + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "Subgraph2") { + { + customer { + __typename + id + identifiers { + id5 + } + mid + } + } + }, + Flatten(path: "customer") { + Fetch(service: "Subgraph3") { + { + ... on Customer { + __typename + id + } + } => + { + ... on Customer { + accounts { + foo(ctx_id5: $contextualArgument_1_0, ctx_mid: $contextualArgument_1_1) { + id + } + } + } + } + }, + }, + }, + } + "### + ); + match plan.node { + Some(TopLevelPlanNode::Sequence(node)) => match node.nodes.get(1) { + Some(PlanNode::Flatten(node)) => match &*node.node { + PlanNode::Fetch(node) => { + assert_eq!( + node.context_rewrites, + vec![ + Arc::new(FetchDataRewrite::KeyRenamer(FetchDataKeyRenamer { + rename_key_to: Name::new("contextualArgument_1_0").unwrap(), + path: vec![ + FetchDataPathElement::Key( + Name::new_unchecked("identifiers"), + Default::default() + ), + FetchDataPathElement::Key( + Name::new_unchecked("id5"), + Default::default() + ), + ], + })), + Arc::new(FetchDataRewrite::KeyRenamer(FetchDataKeyRenamer { + rename_key_to: Name::new("contextualArgument_1_1").unwrap(), + path: vec![FetchDataPathElement::Key( + Name::new_unchecked("mid"), + Default::default() + ),], + })), + ] + ); + } + _ => panic!("failed to get fetch node"), + }, + _ => panic!("failed to get flatten node"), + }, + _ => panic!("failed to get sequence node"), + } +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments.rs index a2d8b785f7..959069588c 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments.rs @@ -1,6 +1,16 @@ +use apollo_federation::query_plan::query_planner::QueryPlannerConfig; + +fn reuse_fragments_config() -> QueryPlannerConfig { + QueryPlannerConfig { + reuse_query_fragments: true, + ..Default::default() + } +} + #[test] fn handles_mix_of_fragments_indirection_and_unions() { let planner = planner!( + config = reuse_fragments_config(), Subgraph1: r#" type Query { parent: Parent @@ -74,6 +84,7 @@ fn another_mix_of_fragments_indirection_and_unions() { // This tests that the issue reported on https://github.com/apollographql/router/issues/3172 is resolved. let planner = planner!( + config = reuse_fragments_config(), Subgraph1: r#" type Query { owner: Owner! @@ -252,6 +263,7 @@ fn another_mix_of_fragments_indirection_and_unions() { #[test] fn handles_fragments_with_interface_field_subtyping() { let planner = planner!( + config = reuse_fragments_config(), Subgraph1: r#" type Query { t1: T1! @@ -313,6 +325,7 @@ fn handles_fragments_with_interface_field_subtyping() { #[test] fn can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_root_fetch() { let planner = planner!( + config = reuse_fragments_config(), Subgraph1: r#" type Query { t1: T @@ -416,6 +429,7 @@ fn can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_root_fetch #[test] fn can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_entity_fetch() { let planner = planner!( + config = reuse_fragments_config(), Subgraph1: r#" type Query { t: T diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments_preservation.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments_preservation.rs index 5633ba79b6..da90c3edb8 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments_preservation.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/named_fragments_preservation.rs @@ -1,8 +1,16 @@ use apollo_federation::query_plan::query_planner::QueryPlannerConfig; +fn reuse_fragments_config() -> QueryPlannerConfig { + QueryPlannerConfig { + reuse_query_fragments: true, + ..Default::default() + } +} + #[test] fn it_works_with_nested_fragments_1() { let planner = planner!( + config = reuse_fragments_config(), Subgraph1: r#" type Query { a: Anything @@ -127,6 +135,7 @@ fn it_works_with_nested_fragments_1() { #[test] fn it_avoid_fragments_usable_only_once() { let planner = planner!( + config = reuse_fragments_config(), Subgraph1: r#" type Query { t: T @@ -324,10 +333,9 @@ mod respects_query_planner_option_reuse_query_fragments { #[test] fn respects_query_planner_option_reuse_query_fragments_true() { - let reuse_query_fragments = true; let planner = planner!( - config = QueryPlannerConfig {reuse_query_fragments, ..Default::default()}, - Subgraph1: SUBGRAPH1, + config = reuse_fragments_config(), + Subgraph1: SUBGRAPH1, ); let query = QUERY; @@ -395,6 +403,7 @@ mod respects_query_planner_option_reuse_query_fragments { #[test] fn it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved() { let planner = planner!( + config = reuse_fragments_config(), Subgraph1: r#" type Query { t: T @@ -463,6 +472,7 @@ fn it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved() fn it_preserves_directives_when_fragment_not_used() { // (because used only once) let planner = planner!( + config = reuse_fragments_config(), Subgraph1: r#" type Query { t: T @@ -511,6 +521,7 @@ fn it_preserves_directives_when_fragment_not_used() { #[test] fn it_preserves_directives_when_fragment_is_reused() { let planner = planner!( + config = reuse_fragments_config(), Subgraph1: r#" type Query { t: T @@ -572,6 +583,7 @@ fn it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgraph() { // server would reject it when validating the query, and we must make sure it // is not reused. let planner = planner!( + config = reuse_fragments_config(), Subgraph1: r#" type Query { i1: I @@ -652,6 +664,7 @@ fn it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_diff // Previous versions of the code were not handling this case and were error out by // creating the invalid selection (#2721), and this test ensures this is fixed. let planner = planner!( + config = reuse_fragments_config(), Subgraph1: r#" type V @shareable { x: Int @@ -988,6 +1001,7 @@ fn it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relati // This test is similar to the subtyping case (it tests the same problems), but test the case // of unions instead of interfaces. let planner = planner!( + config = reuse_fragments_config(), Subgraph1: r#" type V @shareable { x: Int @@ -1302,6 +1316,7 @@ fn it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relati #[test] fn it_preserves_nested_fragments_when_outer_one_has_directives_and_is_eliminated() { let planner = planner!( + config = reuse_fragments_config(), Subgraph1: r#" type Query { t: T diff --git a/apollo-federation/tests/query_plan/supergraphs/add_back_sibling_typename_to_interface_object.graphql b/apollo-federation/tests/query_plan/supergraphs/add_back_sibling_typename_to_interface_object.graphql index 08d3c44ea2..fcf86133d8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/add_back_sibling_typename_to_interface_object.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/add_back_sibling_typename_to_interface_object.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 9c69e32dbbd50fb46ea218c3581b08f4f084b9b9 +# Composed from subgraphs with hash: 36b8aac0fd3b63ab6a2e4d913ce66381d9c97df2 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -46,10 +46,19 @@ interface Item name: String! @join__field(graph: SUBGRAPH1) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/adjacent_mutations_get_merged.graphql b/apollo-federation/tests/query_plan/supergraphs/adjacent_mutations_get_merged.graphql index ae84cc8ed8..6764677f0c 100644 --- a/apollo-federation/tests/query_plan/supergraphs/adjacent_mutations_get_merged.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/adjacent_mutations_get_merged.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 54adb76945715a2ce0e068d2635d71ca4166b289 +# Composed from subgraphs with hash: e7bdd5a089836a642476b6cb289e21dfe867b4ab schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query mutation: Mutation @@ -11,7 +11,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -32,10 +32,19 @@ type Foo baz: Int @join__field(graph: SUBGRAPHB) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPHA @join__graph(name: "SubgraphA", url: "none") SUBGRAPHB @join__graph(name: "SubgraphB", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/allows_setting_down_to_1.graphql b/apollo-federation/tests/query_plan/supergraphs/allows_setting_down_to_1.graphql index 42f3b4b125..a1f388ed61 100644 --- a/apollo-federation/tests/query_plan/supergraphs/allows_setting_down_to_1.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/allows_setting_down_to_1.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 91d4d0661d413b60ae2ed463c074c4180d7b849e +# Composed from subgraphs with hash: a9236eee956ed7fc219b2212696478159ced7eea schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/another_mix_of_fragments_indirection_and_unions.graphql b/apollo-federation/tests/query_plan/supergraphs/another_mix_of_fragments_indirection_and_unions.graphql index 9b6bb51265..d92ae75216 100644 --- a/apollo-federation/tests/query_plan/supergraphs/another_mix_of_fragments_indirection_and_unions.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/another_mix_of_fragments_indirection_and_unions.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: b0bcf31c6c8c64e53e4286dbf397738e41ecbda7 +# Composed from subgraphs with hash: 7e1730cfe16f02de891d008c1310a8355a4b90ab schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,10 +29,19 @@ interface I id2: ID! } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/avoid_considering_indirect_paths_from_the_root_when_a_more_direct_one_exists.graphql b/apollo-federation/tests/query_plan/supergraphs/avoid_considering_indirect_paths_from_the_root_when_a_more_direct_one_exists.graphql index b5d2335e2c..7adf9f6c3b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/avoid_considering_indirect_paths_from_the_root_when_a_more_direct_one_exists.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/avoid_considering_indirect_paths_from_the_root_when_a_more_direct_one_exists.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 9df3e1f539626e2d33ad1aeab8fa51e27fd1f441 +# Composed from subgraphs with hash: 5fba714136085371e4c18ce29483c028c1bcb461 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/avoids_unnecessary_fetches.graphql b/apollo-federation/tests/query_plan/supergraphs/avoids_unnecessary_fetches.graphql index 7ea5c3595d..d9a20ac109 100644 --- a/apollo-federation/tests/query_plan/supergraphs/avoids_unnecessary_fetches.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/avoids_unnecessary_fetches.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 8ba19e06c01ab40b92eda93ac77de08c57856299 +# Composed from subgraphs with hash: f7159b1ede74b4019a8e8f7dfb58369fb4890797 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -32,10 +32,19 @@ type A idA1: ID! @join__field(graph: SUBGRAPH3) @join__field(graph: SUBGRAPH4) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/basic_subscription_query_plan.graphql b/apollo-federation/tests/query_plan/supergraphs/basic_subscription_query_plan.graphql index 8b559e6727..075d5bd978 100644 --- a/apollo-federation/tests/query_plan/supergraphs/basic_subscription_query_plan.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/basic_subscription_query_plan.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: f7613316e8b925d2e1fb7f78b351920adfa4a595 +# Composed from subgraphs with hash: c8a41e00d374bc7c77184e0ffa9fae7d65868f14 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query subscription: Subscription @@ -11,7 +11,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -23,10 +23,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPHA @join__graph(name: "SubgraphA", url: "none") SUBGRAPHB @join__graph(name: "SubgraphB", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/basic_subscription_with_single_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/basic_subscription_with_single_subgraph.graphql index e917252972..221faf8ee5 100644 --- a/apollo-federation/tests/query_plan/supergraphs/basic_subscription_with_single_subgraph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/basic_subscription_with_single_subgraph.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 409fcb3d17fb926642cf9483b5c1db292c219eb1 +# Composed from subgraphs with hash: 8588d7e3fac21b6a30d96678baaf1b49eda0fc99 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query subscription: Subscription @@ -11,7 +11,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -23,10 +23,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPHA @join__graph(name: "SubgraphA", url: "none") SUBGRAPHB @join__graph(name: "SubgraphB", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/can_be_set_to_an_arbitrary_number.graphql b/apollo-federation/tests/query_plan/supergraphs/can_be_set_to_an_arbitrary_number.graphql index 42f3b4b125..a1f388ed61 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_be_set_to_an_arbitrary_number.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_be_set_to_an_arbitrary_number.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 91d4d0661d413b60ae2ed463c074c4180d7b849e +# Composed from subgraphs with hash: a9236eee956ed7fc219b2212696478159ced7eea schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_entity_fetch.graphql b/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_entity_fetch.graphql index 077541aced..fc5780d254 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_entity_fetch.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_entity_fetch.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 24f37faeded16eb10b0ffc6c3c2a0e936f406801 +# Composed from subgraphs with hash: f8053ffafe0a672063ce2538526e3eccba5c20dc schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_root_fetch.graphql b/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_root_fetch.graphql index 4d21c8ec70..a90f6900c4 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_root_fetch.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_root_fetch.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: d4c3168300df54a934d76deb9884e98ba647a676 +# Composed from subgraphs with hash: fe011bd445bfbe40ce53510ba9799480136421c3 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_from_an_interface_object_type.graphql b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_from_an_interface_object_type.graphql index 97189f9365..1226b3f64e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_from_an_interface_object_type.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_from_an_interface_object_type.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd +# Composed from subgraphs with hash: 2d8573a4a560444417df271ed0a39b18c366dbc0 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -51,10 +51,19 @@ interface I y: Int @join__field(graph: S2) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type.graphql b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type.graphql index 97189f9365..1226b3f64e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd +# Composed from subgraphs with hash: 2d8573a4a560444417df271ed0a39b18c366dbc0 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -51,10 +51,19 @@ interface I y: Int @join__field(graph: S2) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation.graphql b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation.graphql index 97189f9365..1226b3f64e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd +# Composed from subgraphs with hash: 2d8573a4a560444417df271ed0a39b18c366dbc0 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -51,10 +51,19 @@ interface I y: Int @join__field(graph: S2) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/can_use_same_root_operation_from_multiple_subgraphs_in_parallel.graphql b/apollo-federation/tests/query_plan/supergraphs/can_use_same_root_operation_from_multiple_subgraphs_in_parallel.graphql index 843563aa3f..d7d10a5dcc 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_use_same_root_operation_from_multiple_subgraphs_in_parallel.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_use_same_root_operation_from_multiple_subgraphs_in_parallel.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 34751a1a79473dcec3aad139f107e5b73a855e0d +# Composed from subgraphs with hash: 40f5afa1d1f5960b7758506956c002b7f3bb3f96 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/condition_order_router799.graphql b/apollo-federation/tests/query_plan/supergraphs/condition_order_router799.graphql index 69ef1b485a..91f7154b37 100644 --- a/apollo-federation/tests/query_plan/supergraphs/condition_order_router799.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/condition_order_router799.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 3ebe0e8c55ad21763879da6341617f14953e80d7 +# Composed from subgraphs with hash: 315fa785fb98377a538827892fc251e5278411df schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query mutation: Mutation @@ -11,7 +11,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -23,10 +23,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { BOOKS @join__graph(name: "books", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/correctly_generate_plan_built_from_some_non_individually_optimal_branch_options.graphql b/apollo-federation/tests/query_plan/supergraphs/correctly_generate_plan_built_from_some_non_individually_optimal_branch_options.graphql index 56e9fef51e..86f3657ed3 100644 --- a/apollo-federation/tests/query_plan/supergraphs/correctly_generate_plan_built_from_some_non_individually_optimal_branch_options.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/correctly_generate_plan_built_from_some_non_individually_optimal_branch_options.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 6ca9a3e7d089605b227c291630c3f089dde1f38d +# Composed from subgraphs with hash: 2a54a92bba655ef6aba52073886d8834898e3f9e schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql b/apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql index 80f221ab6f..7ca7c24f4b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 8a8e74cff42f95e71955aa96d62ef9f9589739d5 +# Composed from subgraphs with hash: 50c5a3e3aada2517a4ded58b107fb6c1f368a9e2 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_gets_stripped_out.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_gets_stripped_out.graphql index ec6c89f59b..e77c1b39e8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_gets_stripped_out.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_gets_stripped_out.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 3e20191edc9bb3ef7d687ce811c347718e6e4100 +# Composed from subgraphs with hash: 71f5545b94f1467afa42deb9d802e13d0d6af30f schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_can_request_typename_in_fragment.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_can_request_typename_in_fragment.graphql index 20cd0d93cd..c7177ab189 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_can_request_typename_in_fragment.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_can_request_typename_in_fragment.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: e2543fc649c80a566b573ebfad36fc0f7458a3a4 +# Composed from subgraphs with hash: 4650b586dd619bfef02a467d67602c065d4b0ed2 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_everything_within_entity.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_everything_within_entity.graphql index 3fb06aff10..b2bdd66659 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_everything_within_entity.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_everything_within_entity.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 428d657ed6389527be73c6ad949cd2fc4da01b20 +# Composed from subgraphs with hash: 22e24356e106228bb20c17c7dabbf476fde27e22 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_multiple_fields_in_different_subgraphs.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_multiple_fields_in_different_subgraphs.graphql index a03862f610..3f2ce69345 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_multiple_fields_in_different_subgraphs.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_multiple_fields_in_different_subgraphs.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 6de814e8aeb455e136ac8627284d67ef4806de57 +# Composed from subgraphs with hash: 15876cef8879238c8dad83fa7976f2925553a806 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_enity_but_with_unuseful_key.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_enity_but_with_unuseful_key.graphql index 3467b225d6..7e45fb8255 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_enity_but_with_unuseful_key.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_enity_but_with_unuseful_key.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: e9d8806fbdfd92a23a7dd2af1e343ff3b6de4a7c +# Composed from subgraphs with hash: a8cdce83720ad36769df084cc55e8b418099bca5 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_everything_queried.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_everything_queried.graphql index 3fb06aff10..b2bdd66659 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_everything_queried.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_everything_queried.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 428d657ed6389527be73c6ad949cd2fc4da01b20 +# Composed from subgraphs with hash: 22e24356e106228bb20c17c7dabbf476fde27e22 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_multi_dependency_deferred_section.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_multi_dependency_deferred_section.graphql index 987e5b30e3..eb886f2ec9 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_multi_dependency_deferred_section.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_multi_dependency_deferred_section.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: cb3bc5e47277ef2c4a364fa62067cfc2426974ec +# Composed from subgraphs with hash: cf4534a5c50af23995f1e5aa82b79e9a858b92bf schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_in_same_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_in_same_subgraph.graphql index e2f0483965..d3740197e4 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_in_same_subgraph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_in_same_subgraph.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: e33e3ea89ff4340ccd53321764ac7f1a2684eb3f +# Composed from subgraphs with hash: 078a72747c6fbfb51e60cd9f7959b3b158495800 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query mutation: Mutation @@ -11,7 +11,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -23,10 +23,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_on_different_subgraphs.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_on_different_subgraphs.graphql index b2e03a0693..950283032c 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_on_different_subgraphs.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_mutation_on_different_subgraphs.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 557c634741b7e9f2f4288edb43724e47f1983d19 +# Composed from subgraphs with hash: d44e187a9a459bd70767ee503e7a1dc4c783d0bf schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query mutation: Mutation @@ -11,7 +11,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -23,10 +23,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_query_root_type.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_query_root_type.graphql index 74e78631de..415d482319 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_query_root_type.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_query_root_type.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 0a1dc62b0c2282030c10f0e0f777f635d6abd3ba +# Composed from subgraphs with hash: 02b05d7c673780361314e10cefea62f6d79d8e3b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -30,10 +30,19 @@ type A next: Query } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_value_types.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_value_types.graphql index 6167ec55b4..45a3a334ff 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_value_types.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_on_value_types.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 01170823ab6c07812976d0983a101d49a319af5c +# Composed from subgraphs with hash: 5cea36a97a40394811ab9ccf97df46c40dcd8ebc schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_only_the_key_of_an_entity.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_only_the_key_of_an_entity.graphql index f41a6d9198..d4a1f50901 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_only_the_key_of_an_entity.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_only_the_key_of_an_entity.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 176bb27a622184464209652e70141da92aeef370 +# Composed from subgraphs with hash: 6b693215fbaac31a45aba14a606333aad808ef8e schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_resuming_in_the_same_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_resuming_in_the_same_subgraph.graphql index 33e9d9c2d9..ba3655a4f0 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_resuming_in_the_same_subgraph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_resuming_in_the_same_subgraph.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 0e99f2e41acb0f707744e78845a55271589146e6 +# Composed from subgraphs with hash: ead6b9a76f52b7caba51acba21fdd37fa52e369a schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_condition_on_single_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_condition_on_single_subgraph.graphql index b185de90ba..56685d5da5 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_condition_on_single_subgraph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_condition_on_single_subgraph.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 59e305d633b6e337422f6431c32cc58defa07302 +# Composed from subgraphs with hash: 6da68bcd4562ba2b8aea44307e29b2b409da8920 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_conditions_and_labels.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_conditions_and_labels.graphql index 20cd0d93cd..c7177ab189 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_conditions_and_labels.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_conditions_and_labels.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: e2543fc649c80a566b573ebfad36fc0f7458a3a4 +# Composed from subgraphs with hash: 4650b586dd619bfef02a467d67602c065d4b0ed2 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_mutliple_conditions_and_labels.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_mutliple_conditions_and_labels.graphql index 1b7e7bdb70..81e36df9a3 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_mutliple_conditions_and_labels.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_defer_with_mutliple_conditions_and_labels.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 2135cbed03439b8658c0113e4663849c991e244b +# Composed from subgraphs with hash: 21f2df1c19b833341b4f9643d34ea0b9cef55ab4 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_entity.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_entity.graphql index 004ed2cf9c..2148644c49 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_entity.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_entity.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 355f15f0f2699fd21731b0403286c513357860d3 +# Composed from subgraphs with hash: 929471dddc80c1a51b87f346eb2f416084d542eb schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_value_type.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_value_type.graphql index 992f860b3a..b73e20a4e4 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_value_type.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_direct_nesting_on_value_type.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: f507628640adf8891453e78dbd4280a132706b11 +# Composed from subgraphs with hash: e99d2671927f9c6a1ee42f44d3c386ade3094009 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_do_not_merge_query_branches_with_defer.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_do_not_merge_query_branches_with_defer.graphql index 6beeae8fd0..6e7383b221 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_do_not_merge_query_branches_with_defer.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_do_not_merge_query_branches_with_defer.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 6af2331e8c3844fbee6c3888b80b44f51cd5bd3d +# Composed from subgraphs with hash: e3be845f74566fd4e434d5d8bf76f6bc7ea62166 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_fragments_expand_into_same_field_regardless_of_defer.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_fragments_expand_into_same_field_regardless_of_defer.graphql index c952a4d998..af0171dca5 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_fragments_expand_into_same_field_regardless_of_defer.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_fragments_expand_into_same_field_regardless_of_defer.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 5382aae137e16d1dfb9955e2a5118b49c75320ea +# Composed from subgraphs with hash: 98071a61d2822a625067b9d1f84c36ca368801d9 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_with_defer_enabled.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_with_defer_enabled.graphql index 385d1b4566..a8a5a9ecfd 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_with_defer_enabled.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_with_defer_enabled.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: f7c59d88291d4be94f4c484dc849118a08361e69 +# Composed from subgraphs with hash: 6f4f2bbb16236373f2eba847b503d19498249c01 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_without_defer_enabled.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_without_defer_enabled.graphql index 385d1b4566..a8a5a9ecfd 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_without_defer_enabled.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_handles_simple_defer_without_defer_enabled.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: f7c59d88291d4be94f4c484dc849118a08361e69 +# Composed from subgraphs with hash: 6f4f2bbb16236373f2eba847b503d19498249c01 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_interface_has_different_definitions_between_subgraphs.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_interface_has_different_definitions_between_subgraphs.graphql index 3fc0c2ca0c..b51cecf205 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_interface_has_different_definitions_between_subgraphs.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_interface_has_different_definitions_between_subgraphs.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: d280d3aae78ad7080c8df8b5a8982d70a4000a78 +# Composed from subgraphs with hash: f2201cf706257dc01dcf09fb0ae827af3b9fa88e schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -31,10 +31,19 @@ interface I b: Int @join__field(graph: SUBGRAPH2) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_multiple_non_nested_defer_plus_label_handling.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_multiple_non_nested_defer_plus_label_handling.graphql index e35ec30241..63b929b522 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_multiple_non_nested_defer_plus_label_handling.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_multiple_non_nested_defer_plus_label_handling.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 09643fc5e0d3abcab8a0f25c0c1e4e16da4169a4 +# Composed from subgraphs with hash: 4c287523c033f1b01e358623f51eb8b9ac3dcd85 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_named_fragments_simple.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_named_fragments_simple.graphql index e8552cbd81..bbb73f12e9 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_named_fragments_simple.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_named_fragments_simple.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 395eb0d8e844d7a54e82eec772f007fafcc37a8e +# Composed from subgraphs with hash: 5f3018ae7e7b98a497ce4fbe8dcd5180c065da06 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_nested_defer_on_entities.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_nested_defer_on_entities.graphql index 10c0f0e1cd..88352b373a 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_nested_defer_on_entities.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_nested_defer_on_entities.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 1ddb9374f772bf962933f20e08543315e8df2b01 +# Composed from subgraphs with hash: b5d17f8a99a3b6c92c1c3f78456e46fadc515cf5 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_one.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_one.graphql index 6f00c992a6..2e67927893 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_one.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_one.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 3bee990c996344aa1eb3a7211e3f497fcbe5e1a6 +# Composed from subgraphs with hash: 3c1a8e679e219bea6bc85b846327ff42bdfac701 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_three.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_three.graphql index b5cdcdd682..0a77a773ea 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_three.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_three.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: c6019a1bd80338506615bb1b60a44fc384586a47 +# Composed from subgraphs with hash: 29206c46cff52b5aadce1935a0f3175d5aacb8dd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_two.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_two.graphql index a132722729..095d8d42b7 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_two.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_non_router_based_defer_case_two.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: fc9fa92848d4ab0e200dcfd09762db2e4281efae +# Composed from subgraphs with hash: a985245292473d8c3e90fc33a9c69754285492cc schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_false.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_false.graphql index 385d1b4566..a8a5a9ecfd 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_false.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_false.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: f7c59d88291d4be94f4c484dc849118a08361e69 +# Composed from subgraphs with hash: 6f4f2bbb16236373f2eba847b503d19498249c01 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_true.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_true.graphql index 385d1b4566..a8a5a9ecfd 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_true.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_normalizes_if_true.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: f7c59d88291d4be94f4c484dc849118a08361e69 +# Composed from subgraphs with hash: 6f4f2bbb16236373f2eba847b503d19498249c01 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_provides_are_ignored_for_deferred_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_provides_are_ignored_for_deferred_fields.graphql index a18f8f7682..7e68977699 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_provides_are_ignored_for_deferred_fields.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_provides_are_ignored_for_deferred_fields.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 39be4a27050623ad7a03d8ae9fed684d1e0f3088 +# Composed from subgraphs with hash: d4bf20edb94edf0cfe2d81956633e26905b36c4e schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_requirements_of_deferred_fields_are_deferred.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_requirements_of_deferred_fields_are_deferred.graphql index 0e2903ca92..6cfc2792cc 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_requirements_of_deferred_fields_are_deferred.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_requirements_of_deferred_fields_are_deferred.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: b3f138a89fd35476e9e36002ae4c8c1ab51c4530 +# Composed from subgraphs with hash: b8bd40a365081d5870913002ff2d4bf0dba0af58 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_test_the_path_in_defer_includes_traversed_fragments.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_test_the_path_in_defer_includes_traversed_fragments.graphql index 82c2cfae78..0075f29bc4 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_test_the_path_in_defer_includes_traversed_fragments.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_test_the_path_in_defer_includes_traversed_fragments.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: eea1ddd3f3e944aaeb5f39f8f9018fd32bcdaaf6 +# Composed from subgraphs with hash: 195dbb671585fe1c25717d53d9d35239abd2e09f schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -36,10 +36,19 @@ interface I x: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/does_not_error_on_some_complex_fetch_group_dependencies.graphql b/apollo-federation/tests/query_plan/supergraphs/does_not_error_on_some_complex_fetch_group_dependencies.graphql index 950263d878..3c79268f77 100644 --- a/apollo-federation/tests/query_plan/supergraphs/does_not_error_on_some_complex_fetch_group_dependencies.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/does_not_error_on_some_complex_fetch_group_dependencies.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 604dead6fd018b16380a1abf51ba199acbdb82f2 +# Composed from subgraphs with hash: c32793dd157c67a6637ceabda719536fc3c703d3 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/does_not_error_out_handling_fragments_when_interface_subtyping_is_involved.graphql b/apollo-federation/tests/query_plan/supergraphs/does_not_error_out_handling_fragments_when_interface_subtyping_is_involved.graphql index 5137fdf735..5e7fdd2848 100644 --- a/apollo-federation/tests/query_plan/supergraphs/does_not_error_out_handling_fragments_when_interface_subtyping_is_involved.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/does_not_error_out_handling_fragments_when_interface_subtyping_is_involved.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 85a2b796ad6f1553ddbc0bf5f4722e602f475090 +# Composed from subgraphs with hash: 7a5c84b7ae6162a788aa569091fcd5a8fe8c4771 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,10 +49,19 @@ interface IB v1: Int! } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/does_not_evaluate_plans_relying_on_a_key_field_to_fetch_that_same_field.graphql b/apollo-federation/tests/query_plan/supergraphs/does_not_evaluate_plans_relying_on_a_key_field_to_fetch_that_same_field.graphql index 674a08c9f2..0aebde659c 100644 --- a/apollo-federation/tests/query_plan/supergraphs/does_not_evaluate_plans_relying_on_a_key_field_to_fetch_that_same_field.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/does_not_evaluate_plans_relying_on_a_key_field_to_fetch_that_same_field.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: e4cc087ad60a261439df7beadccbe9b4a31eaa8d +# Composed from subgraphs with hash: 408f5bf6c20bc49fa52320c529af66fbb36287dd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_for_typename.graphql b/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_for_typename.graphql index 97189f9365..1226b3f64e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_for_typename.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_for_typename.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd +# Composed from subgraphs with hash: 2d8573a4a560444417df271ed0a39b18c366dbc0 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -51,10 +51,19 @@ interface I y: Int @join__field(graph: S2) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is_requested.graphql b/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is_requested.graphql index 97189f9365..1226b3f64e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is_requested.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is_requested.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd +# Composed from subgraphs with hash: 2d8573a4a560444417df271ed0a39b18c366dbc0 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -51,10 +51,19 @@ interface I y: Int @join__field(graph: S2) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql b/apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql index f62dcb21b5..35a2de8da7 100644 --- a/apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 5a37dc86c56cab4caab8bb5d9606eb4c7df0d804 +# Composed from subgraphs with hash: bb70b30ff3918c2d33483331b733c724cbd8631d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + 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") diff --git a/apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql b/apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql index 923bde8335..693e39ab51 100644 --- a/apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 2254345d779de0a2850b6c7a5db722f1989a774c +# Composed from subgraphs with hash: 184d4f82c7a51478107e30ce8aee3d1862c804c2 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -28,10 +28,19 @@ interface Interface field: Interface } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/fields_are_not_overwritten_when_directives_are_removed.graphql b/apollo-federation/tests/query_plan/supergraphs/fields_are_not_overwritten_when_directives_are_removed.graphql index 1b369c434a..2b0526beb0 100644 --- a/apollo-federation/tests/query_plan/supergraphs/fields_are_not_overwritten_when_directives_are_removed.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/fields_are_not_overwritten_when_directives_are_removed.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 654c7bcba86c6f5845a7cb520710cfa37b678e3d +# Composed from subgraphs with hash: 8afd32067575aaae505bac05752941b0314b3cf0 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -35,10 +35,19 @@ type Foo bar: Bar } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPHSKIP @join__graph(name: "SubgraphSkip", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/fragment_with_intersecting_parent_type_and_directive_condition.graphql b/apollo-federation/tests/query_plan/supergraphs/fragment_with_intersecting_parent_type_and_directive_condition.graphql index 1e2a373495..6c5379daff 100644 --- a/apollo-federation/tests/query_plan/supergraphs/fragment_with_intersecting_parent_type_and_directive_condition.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/fragment_with_intersecting_parent_type_and_directive_condition.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: e2393609250b71261acc4089006bc8a14627d488 +# Composed from subgraphs with hash: 60b6f32feef51579a2b534cc06e803a7fd2aa5d8 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,10 +37,19 @@ interface I2 title: String } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { A @join__graph(name: "A", url: "none") B @join__graph(name: "B", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/fragments_that_share_a_hash_but_are_not_identical_generate_their_own_fragment_definitions.graphql b/apollo-federation/tests/query_plan/supergraphs/fragments_that_share_a_hash_but_are_not_identical_generate_their_own_fragment_definitions.graphql index a745ed6263..3e995ed7bb 100644 --- a/apollo-federation/tests/query_plan/supergraphs/fragments_that_share_a_hash_but_are_not_identical_generate_their_own_fragment_definitions.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/fragments_that_share_a_hash_but_are_not_identical_generate_their_own_fragment_definitions.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c +# Composed from subgraphs with hash: d87fc8c7d88d7ddc316b4109f5c0f08a3319255b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -12,7 +12,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -39,10 +39,19 @@ type B z: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql b/apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql index 9b7600e8a4..7206c02130 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: c2ea0958d4d930ae3bf3535692a966af9f3af063 +# Composed from subgraphs with hash: dceeff5b8cc9eef0d5d67d90dcad47c9d2bcdeee schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") NON_GRAPHQL_NAME @join__graph(name: "non-graphql-name", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql b/apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql index 3a1ae9fab2..effff2b959 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 2ad000b79369f5b833cec8d6e5e62e99db89585c +# Composed from subgraphs with hash: 008d1087d7fbef647f430c6050718ba5fd3247db schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { _42_ @join__graph(name: "42!", url: "none") S1 @join__graph(name: "S1", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_case_of_key_chains_in_parallel_requires.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_case_of_key_chains_in_parallel_requires.graphql index fd7953bfd3..bfbe556522 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_case_of_key_chains_in_parallel_requires.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_case_of_key_chains_in_parallel_requires.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 6f7dd1a877d5de533c98948e91197cb3526ed446 +# Composed from subgraphs with hash: 657939f9f9642f8874ed6a99f6d8f0b724c29d6b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_fragments_with_interface_field_subtyping.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_fragments_with_interface_field_subtyping.graphql index 98032aadc8..06e121a504 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_fragments_with_interface_field_subtyping.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_fragments_with_interface_field_subtyping.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: cd5671782860ad7353fd02dfb07664cdc89a5809 +# Composed from subgraphs with hash: 631090cf03bf148115fe36abae4e705737e27a73 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,10 +29,19 @@ interface I other: I! } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_mix_of_fragments_indirection_and_unions.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_mix_of_fragments_indirection_and_unions.graphql index 2ff8417e3b..766e1618ad 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_mix_of_fragments_indirection_and_unions.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_mix_of_fragments_indirection_and_unions.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 0b61d90468ba75e031fdc3c1abe0ac8e87674cc7 +# Composed from subgraphs with hash: 2bc15991aebf8f895f3eb5d24040154b20447182 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -41,10 +41,19 @@ type Child id: ID! } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_multiple_conditions_on_abstract_types.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_conditions_on_abstract_types.graphql index 76280a249e..998237f160 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_multiple_conditions_on_abstract_types.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_conditions_on_abstract_types.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 2bb5218456c710b3aeaf9c5a9a7d87eee258917f +# Composed from subgraphs with hash: ef4b999f84fcdb4baba65f1a44c84d1e4267d948 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -36,10 +36,19 @@ type Book implements Product reviews: [Review!]! @join__field(graph: REVIEWS) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { BOOKS @join__graph(name: "books", url: "none") MAGAZINES @join__graph(name: "magazines", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql index 9a5c273ce0..0a3f854600 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 6120a9371b90c378d13d977d19bad9e17c6875cb +# Composed from subgraphs with hash: 6912a71c206209826d59e92decbc10df806b4164 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -33,10 +33,19 @@ type Item computed2: String @join__field(graph: SUBGRAPH2, requires: "user { value }") } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_non_intersecting_fragment_conditions.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_non_intersecting_fragment_conditions.graphql index 4be5ebe264..c1798df779 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_non_intersecting_fragment_conditions.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_non_intersecting_fragment_conditions.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: cc0d8d93561b6e20d87ae1541b16037be17333ef +# Composed from subgraphs with hash: 3c577bdcdfd12b97d2b265e5ccd5ddf286addb71 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -44,10 +44,19 @@ interface Fruit edible: Boolean! } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_non_matching_value_types_under_interface_field.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_non_matching_value_types_under_interface_field.graphql index 10cc68d004..1f936b1a91 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_non_matching_value_types_under_interface_field.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_non_matching_value_types_under_interface_field.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 9959b044b8b84e47c4559354eb1d87299be28495 +# Composed from subgraphs with hash: 2c44fca1dca09f3611b2c3d037001fdcb2bd607c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -28,10 +28,19 @@ interface I s: S } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_query_of_an_interface_field_for_a_specific_implementation_when_query_starts_with_interface_object.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_query_of_an_interface_field_for_a_specific_implementation_when_query_starts_with_interface_object.graphql index 97189f9365..1226b3f64e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_query_of_an_interface_field_for_a_specific_implementation_when_query_starts_with_interface_object.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_query_of_an_interface_field_for_a_specific_implementation_when_query_starts_with_interface_object.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd +# Composed from subgraphs with hash: 2d8573a4a560444417df271ed0a39b18c366dbc0 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -51,10 +51,19 @@ interface I y: Int @join__field(graph: S2) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_requires_from_supergraph.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_requires_from_supergraph.graphql index 4afa0074ca..0be24065e6 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_requires_from_supergraph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_requires_from_supergraph.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: c980fbf8b4a77ab38f828719197c4878658aa92c +# Composed from subgraphs with hash: aa3ba9dce3c336c185aea3d559466b7d5a3d6602 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,10 +29,19 @@ interface I name: String } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_root_operation_shareable_in_many_subgraphs.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_root_operation_shareable_in_many_subgraphs.graphql index 9c2c5435dd..3cca84dfcc 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_root_operation_shareable_in_many_subgraphs.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_root_operation_shareable_in_many_subgraphs.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: dece71b67cd54bf5e3bf43988e99a638876d52e5 +# Composed from subgraphs with hash: f03aecdc91efa05b3b7a659e31c58bfcd905fb73 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_simple_requires.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_simple_requires.graphql index a7bbfe095a..53d20c5d7e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_simple_requires.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_simple_requires.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: a2964ea427e664c7c007a258b54d68db9abae8f4 +# Composed from subgraphs with hash: d4af614c7debcf4bf2aa8a18f0f23f04412be82d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_spread_unions_correctly.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_spread_unions_correctly.graphql index d0d026a6f3..248bcff72e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_spread_unions_correctly.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_spread_unions_correctly.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 361121049969085d46eae818d8c9d8da18d3cf6c +# Composed from subgraphs with hash: cf0557ca376939819cf4d2f4f7e46d4b3d7836d7 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -47,10 +47,19 @@ type C c2: Int @join__field(graph: SUBGRAPH2) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_types_with_no_common_supertype_at_the_same_merge_at.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_types_with_no_common_supertype_at_the_same_merge_at.graphql index 15ae958653..4a14062e26 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_types_with_no_common_supertype_at_the_same_merge_at.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_types_with_no_common_supertype_at_the_same_merge_at.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 98f941b67e6dc18205c3148808a6bce2b64c775f +# Composed from subgraphs with hash: 8a740dea36b9a39b79632385c08dd53e07af0cbb schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -40,10 +40,19 @@ type Foo y: Int @join__field(graph: SUBGRAPH2) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction.graphql b/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction.graphql index 4c89d271df..0a8c08838e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 6ac1ca5a8c7d1596024e97bb378f1f829788fd71 +# Composed from subgraphs with hash: aad30ec98d465ca592dcfb7b6493a1721589fda7 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -60,10 +60,19 @@ interface I2 v: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction_but_no_need_to_type_explode.graphql b/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction_but_no_need_to_type_explode.graphql index 5433f33dcf..ff8399d64c 100644 --- a/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction_but_no_need_to_type_explode.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction_but_no_need_to_type_explode.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 5073ac23b2d2f5657028261e837e26e4370a3cf4 +# Composed from subgraphs with hash: 6bcf7c1079cc0111e6fc2f4134333b73fc319248 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -60,10 +60,19 @@ interface I2 v: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction.graphql b/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction.graphql index 11afc89b98..bbeaa7ec49 100644 --- a/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 43ca669a61b756dfac8bec5822d87393bc9f3544 +# Composed from subgraphs with hash: 92c4a98434a66db1d0a5cc563bea94c0faaa1a55 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -50,10 +50,19 @@ interface I v: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction_but_no_need_to_type_explode.graphql b/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction_but_no_need_to_type_explode.graphql index 60c26dd523..5d7c1c82c4 100644 --- a/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction_but_no_need_to_type_explode.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction_but_no_need_to_type_explode.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 2548adb14fdc2eacf229834ee87327873c15cb8d +# Composed from subgraphs with hash: e851cb374aa6dea60bffafd658bccd2e017af6f1 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -51,10 +51,19 @@ interface I v: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql b/apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql index fcbc7f2aa3..4f81762ce3 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 7225f76e0549cfa72d92ea3d954fbfa4f5325cff +# Composed from subgraphs with hash: 799c11349d958543e444e53c4ed186a8e7f820f6 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,10 +29,19 @@ interface I b: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_avoid_fragments_usable_only_once.graphql b/apollo-federation/tests/query_plan/supergraphs/it_avoid_fragments_usable_only_once.graphql index 629427d8e7..48a62f633e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_avoid_fragments_usable_only_once.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_avoid_fragments_usable_only_once.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 57cf5fd9ee4bd6e69d9976aef7ff289a74c757c4 +# Composed from subgraphs with hash: 3ea57c667516dce90ad8a2331832f8be1f86f197 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_with_lists.graphql b/apollo-federation/tests/query_plan/supergraphs/it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_with_lists.graphql index b989cee25c..8a8283144b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_with_lists.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_with_lists.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 885b71f276ed574d24895695bddb2bd130640f65 +# Composed from subgraphs with hash: 803bcb0fbcd87892a96ce14b10f3c11256f2870b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -48,10 +48,19 @@ interface I expansiveField: String @join__field(graph: S1) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql index 45bca01a72..22e98bf368 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql @@ -1,8 +1,8 @@ -# Composed from subgraphs with hash: 49f8443a5ff323a1ffed6b7122a1d61e6225d235 +# Composed from subgraphs with hash: 8d7a727bb1c4430ee3ee6df729a5c72ac0a7e181 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) - @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", import: ["@inaccessible"], for: SECURITY) { query: Query } @@ -13,7 +13,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -25,10 +25,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_needlessly_consider_options_for_typename.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_needlessly_consider_options_for_typename.graphql index 82876af15e..e00fa476eb 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_does_not_needlessly_consider_options_for_typename.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_needlessly_consider_options_for_typename.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: ee7dda36e9dc663f9750f86da48822e8f6f6ea8f +# Composed from subgraphs with hash: 665427c4bfc0c14e12d92d34229628661820a58b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql index cda69fb905..054bdac9a1 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +# Composed from subgraphs with hash: 898d84a7356419fd51e15c565a45b3ba121f272f schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "s1", url: "none") S2 @join__graph(name: "s2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql index cda69fb905..054bdac9a1 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +# Composed from subgraphs with hash: 898d84a7356419fd51e15c565a45b3ba121f272f schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "s1", url: "none") S2 @join__graph(name: "s2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql index 206487ca49..b627614bd2 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: b409284c35003f62c7c83675734478b1970effb2 +# Composed from subgraphs with hash: 876e9aeacdff38ab69fae92ab4830e10f28b6fd3 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "s1", url: "none") S2 @join__graph(name: "s2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgraph.graphql index d0358af17b..2f15689f7a 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgraph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgraph.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 11ffa462805ad28daecca693ed1a45d931d3cac8 +# Composed from subgraphs with hash: 48933e92f90d5670a9f24349a530c214cf89fbbc schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -30,10 +30,19 @@ interface I b: Int @join__field(graph: SUBGRAPH2) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_executes_mutation_operations_in_sequence.graphql b/apollo-federation/tests/query_plan/supergraphs/it_executes_mutation_operations_in_sequence.graphql index c8a3b06e68..10ae0d54ce 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_executes_mutation_operations_in_sequence.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_executes_mutation_operations_in_sequence.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: ff6534336a58b9a72232e43fdb66c4769ac4ae66 +# Composed from subgraphs with hash: 167da40652f362ea1ee23360c16a7a706369148d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query mutation: Mutation @@ -11,7 +11,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -23,10 +23,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql index b410e60027..27fd59a787 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 2a90ec602bc41c24462757dc66b2e37e1d96dbf4 +# Composed from subgraphs with hash: a7228e28b556a2f72545f7993f3b68d25944b04d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { A @join__graph(name: "A", url: "none") B @join__graph(name: "B", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql index 7cd49ac90f..4d30aa4ed6 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 88de1465b4ef08a76a910cff26136f931b69eca4 +# Composed from subgraphs with hash: a5459e4976ee54fb002ab54de666b70cd01c9805 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql index 7cd49ac90f..4d30aa4ed6 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 88de1465b4ef08a76a910cff26136f931b69eca4 +# Composed from subgraphs with hash: a5459e4976ee54fb002ab54de666b70cd01c9805 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql index 9442a08abf..91e32419cd 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 213ee593775b6e4a22a852a35da688bc2e85d710 +# Composed from subgraphs with hash: 8d562727336acf24ddefa337d51f03c88d6634de schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -39,10 +39,19 @@ type B c: Int @join__field(graph: SUBGRAPH3, requires: "required") } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql index 8fbe10543a..8070b8da37 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: eeb0b0963b210e2f82e6f40f535037882afb96db +# Composed from subgraphs with hash: f82048b558e108214a7bd9b888b7cd19868588b4 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -40,10 +40,19 @@ type Inner4Type inner4_nested: Int! @join__field(graph: SUBGRAPH5, requires: "inner4_required") } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs.graphql index 39f730f702..8aaa3f274f 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 0545139260d6549f4e32906bce43c37742fdff80 +# Composed from subgraphs with hash: b2221050efb89f6e4df71823675d2ea1fbe66a31 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -38,10 +38,19 @@ type Inner implements I w: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs.graphql index 8706c2af1e..3ffdb5403e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 2be5cb3476eaeac718ff9d77f717b70fe9f344a2 +# Composed from subgraphs with hash: 94b7e94d03865ae730e8e7b6165d4deeed218c72 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,10 +29,19 @@ type Inner w: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragments_with_one_non_leaf_field.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragments_with_one_non_leaf_field.graphql index a745ed6263..3e995ed7bb 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragments_with_one_non_leaf_field.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragments_with_one_non_leaf_field.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c +# Composed from subgraphs with hash: d87fc8c7d88d7ddc316b4109f5c0f08a3319255b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -12,7 +12,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -39,10 +39,19 @@ type B z: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_in_nested_entity.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_in_nested_entity.graphql index fe5bef73a1..15bb01ded8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_in_nested_entity.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_in_nested_entity.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 3881ebaea69bd73941780a88cffefd7f909c77d1 +# Composed from subgraphs with hash: 2dd159285f3b564dab13310cf19c753575fbf1d6 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,10 +49,19 @@ interface I a: Int @join__field(graph: S2) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_input_rewrites_when_cloning_dependency_graph.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_input_rewrites_when_cloning_dependency_graph.graphql index 41b8deead2..19a4cfd986 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_input_rewrites_when_cloning_dependency_graph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_input_rewrites_when_cloning_dependency_graph.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 3799eb12e6387413ef90a73d8848b5c48e40cbca +# Composed from subgraphs with hash: 1b82052bbef41bea0b8cc6650bfd42149af666b5 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -31,10 +31,19 @@ interface I i3: Int @join__field(graph: S2) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql index 0d81bb4ee6..1da9d8d72a 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 00470b1d89f783a11f9b05f82d8377c0d5e3f89d +# Composed from subgraphs with hash: 13b9f66e51175713b45f58b5c9cb1bf93123c731 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH10 @join__graph(name: "Subgraph10", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_with_multiple_fetches.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_with_multiple_fetches.graphql index 57e31110e1..91bd2a3ab0 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_with_multiple_fetches.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_with_multiple_fetches.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 55b2cd6d674b743ae99fa918dce065759899f51d +# Composed from subgraphs with hash: d9d299269fc1b12c334504cd1ba44d173360c498 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -31,10 +31,19 @@ interface I name: String! } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "s1", url: "none") S2 @join__graph(name: "s2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql index 8beeb189bd..48dfe2a166 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 151440cab35934f002ebab673ce5f8bd86966772 +# Composed from subgraphs with hash: 6670829507adac060bea0af75f139243611c4359 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -30,10 +30,19 @@ interface I g: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation.graphql index a745ed6263..3e995ed7bb 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c +# Composed from subgraphs with hash: d87fc8c7d88d7ddc316b4109f5c0f08a3319255b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -12,7 +12,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -39,10 +39,19 @@ type B z: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql index cda69fb905..054bdac9a1 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +# Composed from subgraphs with hash: 898d84a7356419fd51e15c565a45b3ba121f272f schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "s1", url: "none") S2 @join__graph(name: "s2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql index cda69fb905..054bdac9a1 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +# Composed from subgraphs with hash: 898d84a7356419fd51e15c565a45b3ba121f272f schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "s1", url: "none") S2 @join__graph(name: "s2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql index 206487ca49..b627614bd2 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: b409284c35003f62c7c83675734478b1970effb2 +# Composed from subgraphs with hash: 876e9aeacdff38ab69fae92ab4830e10f28b6fd3 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "s1", url: "none") S2 @join__graph(name: "s2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql index 3d4964b712..53ae96662b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: ff92849ceafa9aaccab960b8b5ce0e98a13e6a00 +# Composed from subgraphs with hash: b700dc9de06af55918612c3c23975613c7625ab0 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_requires_on_concrete_type_of_field_provided_by_interface_object.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_requires_on_concrete_type_of_field_provided_by_interface_object.graphql index aae7fd038a..ac4b478cdf 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_requires_on_concrete_type_of_field_provided_by_interface_object.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_requires_on_concrete_type_of_field_provided_by_interface_object.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 209a0436ca69cb640450ef4fc7c204a5b5fc6058 +# Composed from subgraphs with hash: 2a2bbbfde2d57ed4fb71a1b63682f2c3ea5f27e5 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -47,10 +47,19 @@ interface I x: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql index b076bc92fe..119e9a6802 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 244af51d2d8ba7d87e4b9fec74bbb49dc7b00e4d +# Composed from subgraphs with hash: 457e28f8e650826536a34aec9a0d7e7f938609a2 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_identifies_and_reuses_equivalent_fragments_that_arent_identical.graphql b/apollo-federation/tests/query_plan/supergraphs/it_identifies_and_reuses_equivalent_fragments_that_arent_identical.graphql index a745ed6263..3e995ed7bb 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_identifies_and_reuses_equivalent_fragments_that_arent_identical.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_identifies_and_reuses_equivalent_fragments_that_arent_identical.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c +# Composed from subgraphs with hash: d87fc8c7d88d7ddc316b4109f5c0f08a3319255b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -12,7 +12,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -39,10 +39,19 @@ type B z: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/it_migrates_skip_include.graphql b/apollo-federation/tests/query_plan/supergraphs/it_migrates_skip_include.graphql index a745ed6263..3e995ed7bb 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_migrates_skip_include.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_migrates_skip_include.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c +# Composed from subgraphs with hash: d87fc8c7d88d7ddc316b4109f5c0f08a3319255b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -12,7 +12,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -39,10 +39,19 @@ type B z: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql index 8ee0521f3b..e8b4cd762b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +# Composed from subgraphs with hash: 58136a8ddfdf60b78598ff5b86f5b4ae3193a41c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql index 8ee0521f3b..e8b4cd762b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +# Composed from subgraphs with hash: 58136a8ddfdf60b78598ff5b86f5b4ae3193a41c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preservers_aliased_typename.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preservers_aliased_typename.graphql index adc483ebe7..c9885e4c8e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_preservers_aliased_typename.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_preservers_aliased_typename.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: d7fe5a716fee436faefb289f7ba4a8bd05bd7d34 +# Composed from subgraphs with hash: ce2557f5278c94808d1e49ad488bf60d0355e98d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_is_reused.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_is_reused.graphql index 9aa130fc7f..95316d4353 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_is_reused.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_is_reused.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 75955b009750aae84f92b194eddcb553f0e44656 +# Composed from subgraphs with hash: 136ac120ab3c0a9b8ea4cb22cb440886a1b4a961 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_not_used.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_not_used.graphql index 9aa130fc7f..95316d4353 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_not_used.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_not_used.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 75955b009750aae84f92b194eddcb553f0e44656 +# Composed from subgraphs with hash: 136ac120ab3c0a9b8ea4cb22cb440886a1b4a961 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preserves_nested_fragments_when_outer_one_has_directives_and_is_eliminated.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preserves_nested_fragments_when_outer_one_has_directives_and_is_eliminated.graphql index a12329a27d..7b9af26713 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_preserves_nested_fragments_when_outer_one_has_directives_and_is_eliminated.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_preserves_nested_fragments_when_outer_one_has_directives_and_is_eliminated.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: f580fdce2d5df285d6e12633d4355b51e2fd52fd +# Composed from subgraphs with hash: fd162a5fc982fc2cd0a8d33e271831822b681137 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preserves_typename_with_directives.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preserves_typename_with_directives.graphql index 79dbbde767..b812bb841a 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_preserves_typename_with_directives.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_preserves_typename_with_directives.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 5781ed69d05761a7c894a8aac04728581a2475d9 +# Composed from subgraphs with hash: feb9a6756ae190fe90dc2297767c2b4b08fb56a9 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql b/apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql index 66be42e56a..a291d321a8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 3766e23446d1f1881fb155fefa8036726c6f34c4 +# Composed from subgraphs with hash: 01a8e1dfb199705050c7be5bc0f6efb3da20e92e schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { A @join__graph(name: "A", url: "none") B @join__graph(name: "B", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql index 8ee0521f3b..e8b4cd762b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +# Composed from subgraphs with hash: 58136a8ddfdf60b78598ff5b86f5b4ae3193a41c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql index 8ee0521f3b..e8b4cd762b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +# Composed from subgraphs with hash: 58136a8ddfdf60b78598ff5b86f5b4ae3193a41c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_respects_generate_query_fragments_option.graphql b/apollo-federation/tests/query_plan/supergraphs/it_respects_generate_query_fragments_option.graphql index a745ed6263..3e995ed7bb 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_respects_generate_query_fragments_option.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_respects_generate_query_fragments_option.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c +# Composed from subgraphs with hash: d87fc8c7d88d7ddc316b4109f5c0f08a3319255b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -12,7 +12,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -39,10 +39,19 @@ type B z: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql index a7d3b59025..9e11724dcd 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: d654244e34da1e73ea47f5325e371614408d38ae +# Composed from subgraphs with hash: d2db7329336a011305d153d60031e5fe634adedb schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -28,10 +28,19 @@ interface I v: Value } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql index bb44229f8d..51f7ac8d5f 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 87210bff4bf720ff0fe68c22dae1d3e5520d7980 +# Composed from subgraphs with hash: aa33c34a78654942627b836a960f9da314ca826e schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_1.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_1.graphql index 3b5fe2a129..19aedf2358 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_1.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_1.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 18fc379a3170731963d1ec9f54f8b002b0f5d874 +# Composed from subgraphs with hash: 0b52a1cc2cdc06e7b2bb3c438672662d0f80de68 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -64,10 +64,19 @@ interface Foo child2: Foo } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved.graphql index ef1c275632..0d1594dcca 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 197c4ea5c04d17ec60cb084c49efe2f9d662079b +# Composed from subgraphs with hash: af8642bd2cc335a2823e7c95f48ce005d3c809f0 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql index 581e7c764b..9217e787f0 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: fb1513832764f051dd663a966309809fb58e4519 +# Composed from subgraphs with hash: d2ea3c5c49010cc58bc83d22cf064d8e307ad23e schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql index 1b20db25a4..ec70801803 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 0556f39f18edcd446ffee2af4cec39d408bf7b7d +# Composed from subgraphs with hash: bb2856a3e0e2f066120553ce903effcc593c7907 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,10 +37,19 @@ interface I a: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/key_where_at_external_is_not_at_top_level_of_selection_of_requires.graphql b/apollo-federation/tests/query_plan/supergraphs/key_where_at_external_is_not_at_top_level_of_selection_of_requires.graphql index c5c7c136a3..827ec19db0 100644 --- a/apollo-federation/tests/query_plan/supergraphs/key_where_at_external_is_not_at_top_level_of_selection_of_requires.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/key_where_at_external_is_not_at_top_level_of_selection_of_requires.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 3035e486a3ec94c91ff841c2eb464b287fead177 +# Composed from subgraphs with hash: 3517f40ed88e4d3fad368bc9dc78a52173c3f827 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { A @join__graph(name: "A", url: "none") B @join__graph(name: "B", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_order.graphql b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_order.graphql index 80f6562f18..8971d93a09 100644 --- a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_order.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_order.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: fbbe470b5e40af130de42043f74772ec30ee60ff +# Composed from subgraphs with hash: 00f8a26b066b234394aed3ca1e140a534b0364a8 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,10 +29,19 @@ type Hello goodbye: String! } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPHSKIP @join__graph(name: "SubgraphSkip", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_quantity.graphql b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_quantity.graphql index 80f6562f18..8971d93a09 100644 --- a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_quantity.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_quantity.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: fbbe470b5e40af130de42043f74772ec30ee60ff +# Composed from subgraphs with hash: 00f8a26b066b234394aed3ca1e140a534b0364a8 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,10 +29,19 @@ type Hello goodbye: String! } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPHSKIP @join__graph(name: "SubgraphSkip", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_identical.graphql b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_identical.graphql index 80f6562f18..8971d93a09 100644 --- a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_identical.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_identical.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: fbbe470b5e40af130de42043f74772ec30ee60ff +# Composed from subgraphs with hash: 00f8a26b066b234394aed3ca1e140a534b0364a8 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,10 +29,19 @@ type Hello goodbye: String! } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPHSKIP @join__graph(name: "SubgraphSkip", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_with_fragment.graphql b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_with_fragment.graphql index 80f6562f18..8971d93a09 100644 --- a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_with_fragment.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_with_fragment.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: fbbe470b5e40af130de42043f74772ec30ee60ff +# Composed from subgraphs with hash: 00f8a26b066b234394aed3ca1e140a534b0364a8 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,10 +29,19 @@ type Hello goodbye: String! } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPHSKIP @join__graph(name: "SubgraphSkip", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_without_fragment.graphql b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_without_fragment.graphql index 80f6562f18..8971d93a09 100644 --- a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_without_fragment.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_without_fragment.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: fbbe470b5e40af130de42043f74772ec30ee60ff +# Composed from subgraphs with hash: 00f8a26b066b234394aed3ca1e140a534b0364a8 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,10 +29,19 @@ type Hello goodbye: String! } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPHSKIP @join__graph(name: "SubgraphSkip", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/multiplication_overflow_in_reduce_options_if_needed.graphql b/apollo-federation/tests/query_plan/supergraphs/multiplication_overflow_in_reduce_options_if_needed.graphql index 42f3b4b125..a1f388ed61 100644 --- a/apollo-federation/tests/query_plan/supergraphs/multiplication_overflow_in_reduce_options_if_needed.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/multiplication_overflow_in_reduce_options_if_needed.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 91d4d0661d413b60ae2ed463c074c4180d7b849e +# Composed from subgraphs with hash: a9236eee956ed7fc219b2212696478159ced7eea schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/nested_fragment_with_intersecting_parent_type_and_directive_condition.graphql b/apollo-federation/tests/query_plan/supergraphs/nested_fragment_with_intersecting_parent_type_and_directive_condition.graphql index 1e2a373495..6c5379daff 100644 --- a/apollo-federation/tests/query_plan/supergraphs/nested_fragment_with_intersecting_parent_type_and_directive_condition.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/nested_fragment_with_intersecting_parent_type_and_directive_condition.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: e2393609250b71261acc4089006bc8a14627d488 +# Composed from subgraphs with hash: 60b6f32feef51579a2b534cc06e803a7fd2aa5d8 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,10 +37,19 @@ interface I2 title: String } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { A @join__graph(name: "A", url: "none") B @join__graph(name: "B", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/non_adjacent_mutations_do_not_get_merged.graphql b/apollo-federation/tests/query_plan/supergraphs/non_adjacent_mutations_do_not_get_merged.graphql index ae84cc8ed8..6764677f0c 100644 --- a/apollo-federation/tests/query_plan/supergraphs/non_adjacent_mutations_do_not_get_merged.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/non_adjacent_mutations_do_not_get_merged.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 54adb76945715a2ce0e068d2635d71ca4166b289 +# Composed from subgraphs with hash: e7bdd5a089836a642476b6cb289e21dfe867b4ab schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query mutation: Mutation @@ -11,7 +11,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -32,10 +32,19 @@ type Foo baz: Int @join__field(graph: SUBGRAPHB) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPHA @join__graph(name: "SubgraphA", url: "none") SUBGRAPHB @join__graph(name: "SubgraphB", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/only_uses_an_interface_object_if_it_can.graphql b/apollo-federation/tests/query_plan/supergraphs/only_uses_an_interface_object_if_it_can.graphql index 97189f9365..1226b3f64e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/only_uses_an_interface_object_if_it_can.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/only_uses_an_interface_object_if_it_can.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd +# Composed from subgraphs with hash: 2d8573a4a560444417df271ed0a39b18c366dbc0 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -51,10 +51,19 @@ interface I y: Int @join__field(graph: S2) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql b/apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql index dc51cbcdcc..6020cd5b7f 100644 --- a/apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 454b3fc41adce04fc670f06030743d521d09096d +# Composed from subgraphs with hash: 477d072dba9eac87f14e07f061eef2003d803291 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,10 +37,19 @@ type Currency sign: String! } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/rebase_non_intersecting_without_dropping_inline_fragment_due_to_directive.graphql b/apollo-federation/tests/query_plan/supergraphs/rebase_non_intersecting_without_dropping_inline_fragment_due_to_directive.graphql new file mode 100644 index 0000000000..55fea881e2 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/rebase_non_intersecting_without_dropping_inline_fragment_due_to_directive.graphql @@ -0,0 +1,80 @@ +# Composed from subgraphs with hash: 9f65288304601b6cf28091f55f98c58ca3c82972 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +interface I + @join__type(graph: SUBGRAPH1) +{ + i: Int +} + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "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) +{ + test: X +} + +type X implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__type(graph: SUBGRAPH1) +{ + i: Int +} + +type Y implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__type(graph: SUBGRAPH1) +{ + i: Int +} diff --git a/apollo-federation/tests/query_plan/supergraphs/redundant_typename_for_inline_fragments_without_type_condition.graphql b/apollo-federation/tests/query_plan/supergraphs/redundant_typename_for_inline_fragments_without_type_condition.graphql index 923d850187..eb0a2e0d5d 100644 --- a/apollo-federation/tests/query_plan/supergraphs/redundant_typename_for_inline_fragments_without_type_condition.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/redundant_typename_for_inline_fragments_without_type_condition.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: f02b27ab5f92e1e70c07bf7d5ce65e620637ef35 +# Composed from subgraphs with hash: 926811cf9f8aa69f79e2143ad129969648ca0b75 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_false.graphql b/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_false.graphql index 003c7fc0ec..000490dd41 100644 --- a/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_false.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_false.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 4fc759c1b39c54d520a6868db43813e4a38bbaf3 +# Composed from subgraphs with hash: d072d5c57a6c4387ebe527d2ad00a30cbceac34b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,10 +29,19 @@ type A y: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_true.graphql b/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_true.graphql index 003c7fc0ec..000490dd41 100644 --- a/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_true.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_true.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 4fc759c1b39c54d520a6868db43813e4a38bbaf3 +# Composed from subgraphs with hash: d072d5c57a6c4387ebe527d2ad00a30cbceac34b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,10 +29,19 @@ type A y: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/same_as_js_router798.graphql b/apollo-federation/tests/query_plan/supergraphs/same_as_js_router798.graphql index 6895c4bf96..ff31b706b2 100644 --- a/apollo-federation/tests/query_plan/supergraphs/same_as_js_router798.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/same_as_js_router798.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: ee7fce9eb672edf9b036a25bcae0b056ccf5f451 +# Composed from subgraphs with hash: 3606ac3a1b1064419fe78425582e73b5ce3b5369 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -28,10 +28,19 @@ interface Interface a: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") } diff --git a/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql b/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql index 6d2137b561..fc9e5cad10 100644 --- a/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 56ab651bf30bcb3ac7a9fb826fa6b03a57288859 +# Composed from subgraphs with hash: 0d8bac977a97888c29d1c90cff65ea818522aeec schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -38,10 +38,19 @@ type Foo bar: Bar } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/set_context_one_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/set_context_one_subgraph.graphql new file mode 100644 index 0000000000..5a7c7ab74c --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/set_context_one_subgraph.graphql @@ -0,0 +1,87 @@ +# Composed from subgraphs with hash: 493d78ea411e0726dbfcd63da1534851ed24438d +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", import: ["@context"], for: SECURITY) +{ + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: context__ContextFieldValue) on ARGUMENT_DEFINITION + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar context__ContextFieldValue + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "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) + randomId: ID! @join__field(graph: SUBGRAPH2) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @context(name: "Subgraph1__context") +{ + id: ID! + u: U! + prop: String! +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + b: String! + field: Int! @join__field(graph: SUBGRAPH1, contextArguments: [{context: "Subgraph1__context", name: "a", type: "String", selection: " { prop }"}]) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/set_context_required_field_is_several_levels_deep_going_back_and_forth_between_subgraphs.graphql b/apollo-federation/tests/query_plan/supergraphs/set_context_required_field_is_several_levels_deep_going_back_and_forth_between_subgraphs.graphql new file mode 100644 index 0000000000..9fe87e18b6 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/set_context_required_field_is_several_levels_deep_going_back_and_forth_between_subgraphs.graphql @@ -0,0 +1,110 @@ +# Composed from subgraphs with hash: f1dd07e720398727750d4546a6c36b1ee83e38d0 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", import: ["@context"], for: SECURITY) +{ + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: context__ContextFieldValue) on ARGUMENT_DEFINITION + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type A + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + b: B! @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2) +} + +type B + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + c: C! @join__field(graph: SUBGRAPH1) +} + +type C + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + prop: String! +} + +scalar context__ContextFieldValue + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "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) + randomId: ID! @join__field(graph: SUBGRAPH2) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @context(name: "Subgraph1__context") +{ + id: ID! + u: U! + a: A! +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + b: String! + field: Int! @join__field(graph: SUBGRAPH1, contextArguments: [{context: "Subgraph1__context", name: "a", type: "String", selection: " { a { b { c { prop }}} }"}]) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/set_context_test_accesses_a_different_top_level_query.graphql b/apollo-federation/tests/query_plan/supergraphs/set_context_test_accesses_a_different_top_level_query.graphql new file mode 100644 index 0000000000..c911771068 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/set_context_test_accesses_a_different_top_level_query.graphql @@ -0,0 +1,87 @@ +# Composed from subgraphs with hash: ccebd26247c9c39723f64c9ed99609619a002604 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", import: ["@context"], for: SECURITY) +{ + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: context__ContextFieldValue) on ARGUMENT_DEFINITION + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar context__ContextFieldValue + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "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 Product + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + price: Int! @join__field(graph: SUBGRAPH1, contextArguments: [{context: "Subgraph1__topLevelQuery", name: "locale", type: "String", selection: " { me { locale } }"}]) +} + +type Query + @join__type(graph: SUBGRAPH1) + @join__type(graph: SUBGRAPH2) + @context(name: "Subgraph1__topLevelQuery") +{ + me: User! @join__field(graph: SUBGRAPH1) + product: Product @join__field(graph: SUBGRAPH1) + randomId: ID! @join__field(graph: SUBGRAPH2) +} + +type User + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + locale: String! +} diff --git a/apollo-federation/tests/query_plan/supergraphs/set_context_test_before_key_resolution_transition.graphql b/apollo-federation/tests/query_plan/supergraphs/set_context_test_before_key_resolution_transition.graphql new file mode 100644 index 0000000000..496cc9fd8c --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/set_context_test_before_key_resolution_transition.graphql @@ -0,0 +1,95 @@ +# Composed from subgraphs with hash: 70f79d57f189ed5847e652ef9fa7c60603f2d639 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", import: ["@context"], for: SECURITY) +{ + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: context__ContextFieldValue) on ARGUMENT_DEFINITION + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type Child + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + prop: String @join__field(graph: SUBGRAPH2, contextArguments: [{context: "Subgraph2__ctx", name: "legacyUserId", type: "ID", selection: " { identifiers { legacyUserId } }"}]) +} + +scalar context__ContextFieldValue + +type Customer + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") + @context(name: "Subgraph2__ctx") +{ + id: ID! + child: Child! @join__field(graph: SUBGRAPH1) + identifiers: Identifiers! @join__field(graph: SUBGRAPH1) @join__field(graph: SUBGRAPH2, external: true) +} + +type Identifiers + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + legacyUserId: ID! @join__field(graph: SUBGRAPH1) @join__field(graph: SUBGRAPH2, external: true) +} + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "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) +{ + customer: Customer! @join__field(graph: SUBGRAPH1) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/set_context_test_efficiently_merge_fetch_groups.graphql b/apollo-federation/tests/query_plan/supergraphs/set_context_test_efficiently_merge_fetch_groups.graphql new file mode 100644 index 0000000000..d0e79d42e3 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/set_context_test_efficiently_merge_fetch_groups.graphql @@ -0,0 +1,120 @@ +# Composed from subgraphs with hash: 753d6866862484ee27a265b4f2f2f9d4e0c97b03 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", import: ["@context"], for: SECURITY) +{ + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: context__ContextFieldValue) on ARGUMENT_DEFINITION + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type Accounts + @join__type(graph: SUBGRAPH3, key: "id") + @join__type(graph: SUBGRAPH4, key: "id") +{ + foo(randomInput: String): Foo @join__field(graph: SUBGRAPH3, contextArguments: [{context: "Subgraph3__retailCtx", name: "ctx_id5", type: "ID", selection: " { identifiers { id5 } }"}, {context: "Subgraph3__retailCtx", name: "ctx_mid", type: "ID", selection: " { mid }"}]) + id: ID! + bar: Bar @join__field(graph: SUBGRAPH4, contextArguments: [{context: "Subgraph4__widCtx", name: "ctx_wid", type: "ID", selection: " { identifiers { wid } }"}]) +} + +type Bar + @join__type(graph: SUBGRAPH4) +{ + id: ID +} + +scalar context__ContextFieldValue + +type Customer + @join__type(graph: SUBGRAPH2, key: "id") + @join__type(graph: SUBGRAPH3, key: "id") + @join__type(graph: SUBGRAPH4, key: "id", resolvable: false) + @context(name: "Subgraph3__retailCtx") + @context(name: "Subgraph4__widCtx") +{ + id: ID! + identifiers: Identifiers @join__field(graph: SUBGRAPH2) @join__field(graph: SUBGRAPH3, external: true) @join__field(graph: SUBGRAPH4, external: true) + mid: ID @join__field(graph: SUBGRAPH2) @join__field(graph: SUBGRAPH3, external: true) + accounts: Accounts @join__field(graph: SUBGRAPH3) @join__field(graph: SUBGRAPH4) +} + +type Foo + @join__type(graph: SUBGRAPH3) +{ + id: ID +} + +type Identifiers + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") + @join__type(graph: SUBGRAPH3, key: "id") + @join__type(graph: SUBGRAPH4, key: "id", resolvable: false) +{ + id: ID! + id2: ID @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2) + id3: ID @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2) + wid: ID @join__field(graph: SUBGRAPH1, requires: "id2 id3") @join__field(graph: SUBGRAPH4, external: true) + id5: ID @join__field(graph: SUBGRAPH2) @join__field(graph: SUBGRAPH3, external: true) +} + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "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) +{ + customer: Customer @join__field(graph: SUBGRAPH2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/set_context_test_fetched_as_a_list.graphql b/apollo-federation/tests/query_plan/supergraphs/set_context_test_fetched_as_a_list.graphql new file mode 100644 index 0000000000..354ae88c1c --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/set_context_test_fetched_as_a_list.graphql @@ -0,0 +1,88 @@ +# Composed from subgraphs with hash: 8b81d5086b71ff9fb618f2d250eec4b47f7a1d04 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", import: ["@context"], for: SECURITY) +{ + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: context__ContextFieldValue) on ARGUMENT_DEFINITION + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar context__ContextFieldValue + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "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) + a: Int! @join__field(graph: SUBGRAPH2) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @context(name: "Subgraph1__context") +{ + id: ID! + u: U! + prop: String! +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + b: String! @join__field(graph: SUBGRAPH1) + field: Int! @join__field(graph: SUBGRAPH1, contextArguments: [{context: "Subgraph1__context", name: "a", type: "String", selection: " { prop }"}]) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/set_context_test_impacts_on_query_planning.graphql b/apollo-federation/tests/query_plan/supergraphs/set_context_test_impacts_on_query_planning.graphql new file mode 100644 index 0000000000..596c21edad --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/set_context_test_impacts_on_query_planning.graphql @@ -0,0 +1,106 @@ +# Composed from subgraphs with hash: 173623d59b042e5f8dcf81f5c08880ad2d2d3ccb +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", import: ["@context"], for: SECURITY) +{ + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: context__ContextFieldValue) on ARGUMENT_DEFINITION + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type A implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + u: U! + prop: String! +} + +type B implements I + @join__implements(graph: SUBGRAPH1, interface: "I") + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + u: U! + prop: String! +} + +scalar context__ContextFieldValue + +interface I + @join__type(graph: SUBGRAPH1, key: "id") + @context(name: "Subgraph1__context") +{ + id: ID! + u: U! + prop: String! +} + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "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: I! @join__field(graph: SUBGRAPH1) + a: Int! @join__field(graph: SUBGRAPH2) +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + b: String! @join__field(graph: SUBGRAPH1) + field: Int! @join__field(graph: SUBGRAPH1, contextArguments: [{context: "Subgraph1__context", name: "a", type: "String", selection: " { prop }"}]) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/set_context_test_variable_is_a_list.graphql b/apollo-federation/tests/query_plan/supergraphs/set_context_test_variable_is_a_list.graphql new file mode 100644 index 0000000000..d502c7d4b1 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/set_context_test_variable_is_a_list.graphql @@ -0,0 +1,87 @@ +# Composed from subgraphs with hash: 1a8895fe791cc7cd69ace02dc95527034d7d864f +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", import: ["@context"], for: SECURITY) +{ + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: context__ContextFieldValue) on ARGUMENT_DEFINITION + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar context__ContextFieldValue + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "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) + a: Int! @join__field(graph: SUBGRAPH2) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @context(name: "Subgraph1__context") +{ + id: ID! + u: U! + prop: [String]! +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + field: Int! @join__field(graph: SUBGRAPH1, contextArguments: [{context: "Subgraph1__context", name: "a", type: "[String]", selection: " { prop }"}]) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/set_context_test_variable_is_already_in_a_different_fetch_group.graphql b/apollo-federation/tests/query_plan/supergraphs/set_context_test_variable_is_already_in_a_different_fetch_group.graphql new file mode 100644 index 0000000000..55fbffc7dd --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/set_context_test_variable_is_already_in_a_different_fetch_group.graphql @@ -0,0 +1,88 @@ +# Composed from subgraphs with hash: 295947c45a4fbf129c3112e24611d04c40619c86 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", import: ["@context"], for: SECURITY) +{ + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: context__ContextFieldValue) on ARGUMENT_DEFINITION + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar context__ContextFieldValue + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "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) + a: Int! @join__field(graph: SUBGRAPH2) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") + @context(name: "Subgraph2__context") +{ + id: ID! + u: U! @join__field(graph: SUBGRAPH1) + prop: String! @join__field(graph: SUBGRAPH1) @join__field(graph: SUBGRAPH2, external: true) +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + field: Int! @join__field(graph: SUBGRAPH2, contextArguments: [{context: "Subgraph2__context", name: "a", type: "String", selection: " { prop }"}]) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/set_context_test_variable_is_from_different_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/set_context_test_variable_is_from_different_subgraph.graphql new file mode 100644 index 0000000000..f71c83f1a3 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/set_context_test_variable_is_from_different_subgraph.graphql @@ -0,0 +1,88 @@ +# Composed from subgraphs with hash: a1eeaafd2a79733109742acf5e08df586d1358b0 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", import: ["@context"], for: SECURITY) +{ + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: context__ContextFieldValue) on ARGUMENT_DEFINITION + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar context__ContextFieldValue + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "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) + a: Int! @join__field(graph: SUBGRAPH2) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") + @context(name: "Subgraph1__context") +{ + id: ID! + u: U! @join__field(graph: SUBGRAPH1) + prop: String! @join__field(graph: SUBGRAPH1, external: true) @join__field(graph: SUBGRAPH2) +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + field: Int! @join__field(graph: SUBGRAPH1, contextArguments: [{context: "Subgraph1__context", name: "a", type: "String", selection: " { prop }"}]) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/set_context_test_variable_is_from_same_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/set_context_test_variable_is_from_same_subgraph.graphql new file mode 100644 index 0000000000..24d777888d --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/set_context_test_variable_is_from_same_subgraph.graphql @@ -0,0 +1,88 @@ +# Composed from subgraphs with hash: 2de3c6cc9b36392a362af0d19e03688085e2119b +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", import: ["@context"], for: SECURITY) +{ + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: context__ContextFieldValue) on ARGUMENT_DEFINITION + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar context__ContextFieldValue + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "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) + a: Int! @join__field(graph: SUBGRAPH2) +} + +type T + @join__type(graph: SUBGRAPH1, key: "id") + @context(name: "Subgraph1__context") +{ + id: ID! + u: U! + prop: String! +} + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + b: String! @join__field(graph: SUBGRAPH1) + field: Int! @join__field(graph: SUBGRAPH1, contextArguments: [{context: "Subgraph1__context", name: "a", type: "String", selection: " { prop }"}]) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/set_context_test_with_type_conditions_for_union.graphql b/apollo-federation/tests/query_plan/supergraphs/set_context_test_with_type_conditions_for_union.graphql new file mode 100644 index 0000000000..ae73fa590c --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/set_context_test_with_type_conditions_for_union.graphql @@ -0,0 +1,102 @@ +# Composed from subgraphs with hash: 0981934ba0944ccff6a8c554bef807ca905ad13a +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @link(url: "https://specs.apollo.dev/context/v0.1", import: ["@context"], for: SECURITY) +{ + query: Query +} + +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION + +directive @context__fromContext(field: context__ContextFieldValue) on ARGUMENT_DEFINITION + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type A + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + u: U! + prop: String! +} + +type B + @join__type(graph: SUBGRAPH1, key: "id") +{ + id: ID! + u: U! + prop: String! +} + +scalar context__ContextFieldValue + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "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) + a: Int! @join__field(graph: SUBGRAPH2) +} + +union T + @join__type(graph: SUBGRAPH1) + @join__unionMember(graph: SUBGRAPH1, member: "A") + @join__unionMember(graph: SUBGRAPH1, member: "B") + @context(name: "Subgraph1__context") + = A | B + +type U + @join__type(graph: SUBGRAPH1, key: "id") + @join__type(graph: SUBGRAPH2, key: "id") +{ + id: ID! + b: String! @join__field(graph: SUBGRAPH1) + field: Int! @join__field(graph: SUBGRAPH1, contextArguments: [{context: "Subgraph1__context", name: "a", type: "String", selection: " ... on A { prop } ... on B { prop }"}]) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/skip_type_explosion_early_if_unnecessary.graphql b/apollo-federation/tests/query_plan/supergraphs/skip_type_explosion_early_if_unnecessary.graphql index c0db4184d4..4802078dad 100644 --- a/apollo-federation/tests/query_plan/supergraphs/skip_type_explosion_early_if_unnecessary.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/skip_type_explosion_early_if_unnecessary.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 63eb1eb88aa472162170c24e74ca47c7c4ac1866 +# Composed from subgraphs with hash: 22a84f887ef58f251ff2c8f6439dcdfbdc1395fd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -28,10 +28,19 @@ interface I s: S } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/subgraph_query_retains_the_query_variables_used_in_the_directives_applied_to_the_query.graphql b/apollo-federation/tests/query_plan/supergraphs/subgraph_query_retains_the_query_variables_used_in_the_directives_applied_to_the_query.graphql index 67fa096ee1..000717825c 100644 --- a/apollo-federation/tests/query_plan/supergraphs/subgraph_query_retains_the_query_variables_used_in_the_directives_applied_to_the_query.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/subgraph_query_retains_the_query_variables_used_in_the_directives_applied_to_the_query.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 64759e825a65c99c54fedcbf511cc7bf0735d4e2 +# Composed from subgraphs with hash: b577e09c4cb52223182c48413d146f984b798808 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -24,10 +24,19 @@ directive @link(url: String, as: String, for: link__Purpose, import: [link__Impo directive @withArgs(arg1: String) on QUERY +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_at_the_operation_level_are_passed_down_to_subgraph_queries.graphql b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_at_the_operation_level_are_passed_down_to_subgraph_queries.graphql index f2012f3f04..b7cd3853f8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_at_the_operation_level_are_passed_down_to_subgraph_queries.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_at_the_operation_level_are_passed_down_to_subgraph_queries.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: a0524dbe1bbd3a7450a2e15f5c25c5cf2eed4242 +# Composed from subgraphs with hash: cc7760ea5772ca757685d5802613391331d0bb89 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query mutation: Mutation @@ -13,7 +13,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,10 +37,19 @@ type Foo baz: Int @join__field(graph: SUBGRAPHB) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPHA @join__graph(name: "subgraphA", url: "none") SUBGRAPHB @join__graph(name: "subgraphB", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_on_mutations_are_passed_down_to_subgraph_queries.graphql b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_on_mutations_are_passed_down_to_subgraph_queries.graphql index f2012f3f04..b7cd3853f8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_on_mutations_are_passed_down_to_subgraph_queries.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_on_mutations_are_passed_down_to_subgraph_queries.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: a0524dbe1bbd3a7450a2e15f5c25c5cf2eed4242 +# Composed from subgraphs with hash: cc7760ea5772ca757685d5802613391331d0bb89 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query mutation: Mutation @@ -13,7 +13,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,10 +37,19 @@ type Foo baz: Int @join__field(graph: SUBGRAPHB) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPHA @join__graph(name: "subgraphA", url: "none") SUBGRAPHB @join__graph(name: "subgraphB", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_with_arguments_applied_on_queries_are_ok.graphql b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_with_arguments_applied_on_queries_are_ok.graphql index 5eb84bed66..088d93fd2e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_with_arguments_applied_on_queries_are_ok.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_with_arguments_applied_on_queries_are_ok.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 5338f8c604ab7c2103bdf58ee6752a40d4b62ed8 +# Composed from subgraphs with hash: 835b813ecafe8b6b6bbffd9b802895d83cacac08 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -26,10 +26,19 @@ directive @noArgs on QUERY directive @withArgs(arg1: String) on QUERY +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/test_indirect_branch_merging_with_typename_sibling.graphql b/apollo-federation/tests/query_plan/supergraphs/test_indirect_branch_merging_with_typename_sibling.graphql index 462629b77a..2f2ab91aa6 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_indirect_branch_merging_with_typename_sibling.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_indirect_branch_merging_with_typename_sibling.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 9be0826e3b911556466c2c410f7df8b53c241774 +# Composed from subgraphs with hash: efc8cbf9c39df8acdc21d7cb3fb9e23700650a82 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -42,10 +42,19 @@ type B implements T f: Int! @join__field(graph: SUBGRAPH2) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/test_interface_object_advance_with_non_collecting_and_type_preserving_transitions_ordering.graphql b/apollo-federation/tests/query_plan/supergraphs/test_interface_object_advance_with_non_collecting_and_type_preserving_transitions_ordering.graphql index da08220702..eb112a8850 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_interface_object_advance_with_non_collecting_and_type_preserving_transitions_ordering.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_interface_object_advance_with_non_collecting_and_type_preserving_transitions_ordering.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 415e22ad50245441330e67dc6637cf09714a4831 +# Composed from subgraphs with hash: 7c59bdaefc39d7e88a603f0560376a398e5b7e93 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -57,10 +57,19 @@ interface I data: String! @join__field(graph: Z) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { S1 @join__graph(name: "S1", url: "none") S2 @join__graph(name: "S2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_do_not_create_cycle_in_fetch_dependency_graph.graphql b/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_do_not_create_cycle_in_fetch_dependency_graph.graphql index 82e69229f1..c92362b1c7 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_do_not_create_cycle_in_fetch_dependency_graph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_do_not_create_cycle_in_fetch_dependency_graph.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 0b567f1d7e0089a146e8499a2c5e412b78a98498 +# Composed from subgraphs with hash: 806d47884a3b16cd6552156d332df34cb74e0ffc schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { A @join__graph(name: "A", url: "none") B @join__graph(name: "B", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_reset_cached_costs.graphql b/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_reset_cached_costs.graphql index 6b66fcbf6e..6f34ea9a77 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_reset_cached_costs.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_reset_cached_costs.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 7c07647747a84fd6c839bb603948dddcadf654fd +# Composed from subgraphs with hash: 0cd24ae6074a39994c982e5fa519acabfe2dcdac schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { A @join__graph(name: "A", url: "none") B @join__graph(name: "B", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/test_provides_edge_ordering.graphql b/apollo-federation/tests/query_plan/supergraphs/test_provides_edge_ordering.graphql index 6e2401f7d4..002b35a308 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_provides_edge_ordering.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_provides_edge_ordering.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: f5cb1210587d45fee11b9c57247d6c570d0ae7fd +# Composed from subgraphs with hash: 44564f95d87b3306e4c708b71f30f050c48d3a2d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -31,10 +31,19 @@ type A data: String! @join__field(graph: SUBGRAPHX) @join__field(graph: SUBGRAPHY) } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPHQ @join__graph(name: "SubgraphQ", url: "none") SUBGRAPHX @join__graph(name: "SubgraphX", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/trying_to_use_defer_with_a_subcription_results_in_an_error.graphql b/apollo-federation/tests/query_plan/supergraphs/trying_to_use_defer_with_a_subcription_results_in_an_error.graphql index 1d901c0ee1..5958431c2c 100644 --- a/apollo-federation/tests/query_plan/supergraphs/trying_to_use_defer_with_a_subcription_results_in_an_error.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/trying_to_use_defer_with_a_subcription_results_in_an_error.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 4c8b155cb8183b493b5c2862a2bd4ce0261642f9 +# Composed from subgraphs with hash: ebc3c557daba5b588e6f31c98db4dea0296b99e7 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query subscription: Subscription @@ -11,7 +11,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -23,10 +23,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPHA @join__graph(name: "SubgraphA", url: "none") SUBGRAPHB @join__graph(name: "SubgraphB", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction.graphql b/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction.graphql index 347f1bcfb8..d8b17e29b5 100644 --- a/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 336400b353f835910c178a10eb77371f8700396b +# Composed from subgraphs with hash: a36c94877a2b170f973a35c58634ef4ee999c103 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -51,10 +51,19 @@ interface I v: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction_but_no_need_to_type_explode.graphql b/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction_but_no_need_to_type_explode.graphql index 3dfc5d39b8..292658a75e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction_but_no_need_to_type_explode.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction_but_no_need_to_type_explode.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 0457b8e67ae0ea99ead4c13318c4ac89e821aac3 +# Composed from subgraphs with hash: 82e74064026e626dde5798a7eaa1e6426c2c51d9 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -50,10 +50,19 @@ interface I v: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/union_union_interaction.graphql b/apollo-federation/tests/query_plan/supergraphs/union_union_interaction.graphql index bb7bf23e98..3e41a51d06 100644 --- a/apollo-federation/tests/query_plan/supergraphs/union_union_interaction.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/union_union_interaction.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 997336a5c7996069d8c10d0b9be46a72b46459c1 +# Composed from subgraphs with hash: 02b257e0662ad5b58d0147cb3c34c295418df23c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -41,10 +41,19 @@ type C v: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/union_union_interaction_but_no_need_to_type_explode.graphql b/apollo-federation/tests/query_plan/supergraphs/union_union_interaction_but_no_need_to_type_explode.graphql index 3dd79adf66..aed475b293 100644 --- a/apollo-federation/tests/query_plan/supergraphs/union_union_interaction_but_no_need_to_type_explode.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/union_union_interaction_but_no_need_to_type_explode.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 241b1f7314833f7c2a97da8176e258c40322b0d7 +# Composed from subgraphs with hash: affd505df6fdd709ffe38651ddc517c8f33ec727 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -41,10 +41,19 @@ type C v: Int } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql b/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql index 33f5dd6f05..3c2493c9c8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: e6c72fb53e93abe8f8aed4982aca6f6109fe1171 +# Composed from subgraphs with hash: 7267ff8701477b9b37e32de3063a369f4a7e2af3 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,10 +37,19 @@ type Foo bar: Bar } +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/works_when_unset.graphql b/apollo-federation/tests/query_plan/supergraphs/works_when_unset.graphql index 42f3b4b125..a1f388ed61 100644 --- a/apollo-federation/tests/query_plan/supergraphs/works_when_unset.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/works_when_unset.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 91d4d0661d413b60ae2ed463c074c4180d7b849e +# Composed from subgraphs with hash: a9236eee956ed7fc219b2212696478159ced7eea schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/query_plan/supergraphs/works_with_key_chains.graphql b/apollo-federation/tests/query_plan/supergraphs/works_with_key_chains.graphql index 48816b3a72..3b8b99fd6d 100644 --- a/apollo-federation/tests/query_plan/supergraphs/works_with_key_chains.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/works_with_key_chains.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 4387e2f917c7748296b92799227648747e453bec +# Composed from subgraphs with hash: 2a34e202493c546249d10e9e361038512dd0e213 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { query: Query } @@ -10,7 +10,7 @@ directive @join__directive(graphs: [join__Graph!], name: String!, args: join__Di directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,10 +22,19 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + scalar join__DirectiveArguments scalar join__FieldSet +scalar join__FieldValue + enum join__Graph { SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none") SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none") diff --git a/apollo-federation/tests/snapshots/main__extract_subgraphs__can_extract_subgraph.snap b/apollo-federation/tests/snapshots/main__extract_subgraphs__can_extract_subgraph.snap index 324709bd56..50474161ef 100644 --- a/apollo-federation/tests/snapshots/main__extract_subgraphs__can_extract_subgraph.snap +++ b/apollo-federation/tests/snapshots/main__extract_subgraphs__can_extract_subgraph.snap @@ -42,6 +42,10 @@ directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_ directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION +directive @federation__fromContext(field: String) on ARGUMENT_DEFINITION + +directive @federation__context(name: String) repeatable on INTERFACE | OBJECT | UNION + scalar link__Import enum link__Purpose { @@ -123,6 +127,10 @@ directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_ directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION +directive @federation__fromContext(field: String) on ARGUMENT_DEFINITION + +directive @federation__context(name: String) repeatable on INTERFACE | OBJECT | UNION + scalar link__Import enum link__Purpose { diff --git a/apollo-federation/tests/snapshots/main__extract_subgraphs__does_not_extract_demand_control_directive_name_conflicts.snap b/apollo-federation/tests/snapshots/main__extract_subgraphs__does_not_extract_demand_control_directive_name_conflicts.snap index f86e759fca..a20ce0e66d 100644 --- a/apollo-federation/tests/snapshots/main__extract_subgraphs__does_not_extract_demand_control_directive_name_conflicts.snap +++ b/apollo-federation/tests/snapshots/main__extract_subgraphs__does_not_extract_demand_control_directive_name_conflicts.snap @@ -42,6 +42,10 @@ directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_ directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION +directive @federation__fromContext(field: String) on ARGUMENT_DEFINITION + +directive @federation__context(name: String) repeatable on INTERFACE | OBJECT | UNION + scalar link__Import enum link__Purpose { @@ -112,6 +116,10 @@ directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_ directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION +directive @federation__fromContext(field: String) on ARGUMENT_DEFINITION + +directive @federation__context(name: String) repeatable on INTERFACE | OBJECT | UNION + scalar link__Import enum link__Purpose { diff --git a/apollo-federation/tests/snapshots/main__extract_subgraphs__does_not_extract_renamed_demand_control_directive_name_conflicts.snap b/apollo-federation/tests/snapshots/main__extract_subgraphs__does_not_extract_renamed_demand_control_directive_name_conflicts.snap index f86e759fca..a20ce0e66d 100644 --- a/apollo-federation/tests/snapshots/main__extract_subgraphs__does_not_extract_renamed_demand_control_directive_name_conflicts.snap +++ b/apollo-federation/tests/snapshots/main__extract_subgraphs__does_not_extract_renamed_demand_control_directive_name_conflicts.snap @@ -42,6 +42,10 @@ directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_ directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION +directive @federation__fromContext(field: String) on ARGUMENT_DEFINITION + +directive @federation__context(name: String) repeatable on INTERFACE | OBJECT | UNION + scalar link__Import enum link__Purpose { @@ -112,6 +116,10 @@ directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_ directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION +directive @federation__fromContext(field: String) on ARGUMENT_DEFINITION + +directive @federation__context(name: String) repeatable on INTERFACE | OBJECT | UNION + scalar link__Import enum link__Purpose { diff --git a/apollo-federation/tests/snapshots/main__extract_subgraphs__extracts_demand_control_directives.snap b/apollo-federation/tests/snapshots/main__extract_subgraphs__extracts_demand_control_directives.snap index 319b91d908..c32e8c73a9 100644 --- a/apollo-federation/tests/snapshots/main__extract_subgraphs__extracts_demand_control_directives.snap +++ b/apollo-federation/tests/snapshots/main__extract_subgraphs__extracts_demand_control_directives.snap @@ -42,6 +42,10 @@ directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_ directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION +directive @federation__fromContext(field: String) on ARGUMENT_DEFINITION + +directive @federation__context(name: String) repeatable on INTERFACE | OBJECT | UNION + scalar link__Import enum link__Purpose { @@ -132,6 +136,10 @@ directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_ directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION +directive @federation__fromContext(field: String) on ARGUMENT_DEFINITION + +directive @federation__context(name: String) repeatable on INTERFACE | OBJECT | UNION + scalar link__Import enum link__Purpose { diff --git a/apollo-federation/tests/snapshots/main__extract_subgraphs__extracts_renamed_demand_control_directives.snap b/apollo-federation/tests/snapshots/main__extract_subgraphs__extracts_renamed_demand_control_directives.snap index 319b91d908..c32e8c73a9 100644 --- a/apollo-federation/tests/snapshots/main__extract_subgraphs__extracts_renamed_demand_control_directives.snap +++ b/apollo-federation/tests/snapshots/main__extract_subgraphs__extracts_renamed_demand_control_directives.snap @@ -42,6 +42,10 @@ directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_ directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION +directive @federation__fromContext(field: String) on ARGUMENT_DEFINITION + +directive @federation__context(name: String) repeatable on INTERFACE | OBJECT | UNION + scalar link__Import enum link__Purpose { @@ -132,6 +136,10 @@ directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_ directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION +directive @federation__fromContext(field: String) on ARGUMENT_DEFINITION + +directive @federation__context(name: String) repeatable on INTERFACE | OBJECT | UNION + scalar link__Import enum link__Purpose { diff --git a/apollo-federation/tests/snapshots/main__extract_subgraphs__extracts_set_context_directives.snap b/apollo-federation/tests/snapshots/main__extract_subgraphs__extracts_set_context_directives.snap new file mode 100644 index 0000000000..fd7c25bff1 --- /dev/null +++ b/apollo-federation/tests/snapshots/main__extract_subgraphs__extracts_set_context_directives.snap @@ -0,0 +1,170 @@ +--- +source: apollo-federation/tests/extract_subgraphs.rs +expression: snapshot +--- +Subgraph1 +--- +schema { + query: Query +} + +extend schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/federation/v2.9") + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +directive @federation__key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE + +directive @federation__requires(fields: federation__FieldSet!) on FIELD_DEFINITION + +directive @federation__provides(fields: federation__FieldSet!) on FIELD_DEFINITION + +directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION + +directive @federation__tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA + +directive @federation__extends on OBJECT | INTERFACE + +directive @federation__shareable on OBJECT | FIELD_DEFINITION + +directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +directive @federation__override(from: String!, label: String) on FIELD_DEFINITION + +directive @federation__composeDirective(name: String) repeatable on SCHEMA + +directive @federation__interfaceObject on OBJECT + +directive @federation__authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM + +directive @federation__requiresScopes(scopes: [[federation__Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM + +directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR + +directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION + +directive @federation__fromContext(field: String) on ARGUMENT_DEFINITION + +directive @federation__context(name: String) repeatable on INTERFACE | OBJECT | UNION + +scalar link__Import + +enum link__Purpose { + """ + \`SECURITY\` features provide metadata necessary to securely resolve fields. + """ + SECURITY + """ + \`EXECUTION\` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +scalar federation__FieldSet + +scalar federation__Scope + +type Query { + t: T! + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type T @federation__key(fields: "id", resolvable: true) @federation__context(name: "context") { + id: ID! + u: U! + prop: String! +} + +type U @federation__key(fields: "id", resolvable: true) { + id: ID! @federation__shareable + field( + a: String @federation__fromContext(field: "$context { prop }"), + ): Int! +} + +scalar _Any + +type _Service { + sdl: String +} + +union _Entity = T | U + +Subgraph2 +--- +schema { + query: Query +} + +extend schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/federation/v2.9") + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +directive @federation__key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE + +directive @federation__requires(fields: federation__FieldSet!) on FIELD_DEFINITION + +directive @federation__provides(fields: federation__FieldSet!) on FIELD_DEFINITION + +directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION + +directive @federation__tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA + +directive @federation__extends on OBJECT | INTERFACE + +directive @federation__shareable on OBJECT | FIELD_DEFINITION + +directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +directive @federation__override(from: String!, label: String) on FIELD_DEFINITION + +directive @federation__composeDirective(name: String) repeatable on SCHEMA + +directive @federation__interfaceObject on OBJECT + +directive @federation__authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM + +directive @federation__requiresScopes(scopes: [[federation__Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM + +directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR + +directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION + +directive @federation__fromContext(field: String) on ARGUMENT_DEFINITION + +directive @federation__context(name: String) repeatable on INTERFACE | OBJECT | UNION + +scalar link__Import + +enum link__Purpose { + """ + \`SECURITY\` features provide metadata necessary to securely resolve fields. + """ + SECURITY + """ + \`EXECUTION\` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +scalar federation__FieldSet + +scalar federation__Scope + +type Query { + a: Int! + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type U @federation__key(fields: "id", resolvable: true) { + id: ID! @federation__shareable +} + +scalar _Any + +type _Service { + sdl: String +} + +union _Entity = U diff --git a/apollo-router-benchmarks/Cargo.toml b/apollo-router-benchmarks/Cargo.toml index d26748c696..359391b4a7 100644 --- a/apollo-router-benchmarks/Cargo.toml +++ b/apollo-router-benchmarks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-benchmarks" -version = "1.57.1" +version = "1.58.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-benchmarks/src/shared.rs b/apollo-router-benchmarks/src/shared.rs index cf3e57af9b..e0576c603e 100644 --- a/apollo-router-benchmarks/src/shared.rs +++ b/apollo-router-benchmarks/src/shared.rs @@ -76,7 +76,7 @@ pub fn setup() -> TestHarness<'static> { }}).build(); let review_service = MockSubgraph::builder().with_json(json!{{ - "query": "query TopProducts__reviews__1($representations:[_Any!]!){_entities(representations:$representations){...on Product{reviews{id product{__typename upc}author{__typename id}}}}}", + "query": "query TopProducts__reviews__1($representations:[_Any!]!){_entities(representations:$representations){..._generated_onProduct1_0}}fragment _generated_onProduct1_0 on Product{reviews{id product{__typename upc}author{__typename id}}}", "operationName": "TopProducts__reviews__1", "variables": { "representations":[ diff --git a/apollo-router-scaffold/Cargo.toml b/apollo-router-scaffold/Cargo.toml index 63bf887d7f..8b68e56a12 100644 --- a/apollo-router-scaffold/Cargo.toml +++ b/apollo-router-scaffold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-scaffold" -version = "1.57.1" +version = "1.58.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/templates/base/Cargo.template.toml b/apollo-router-scaffold/templates/base/Cargo.template.toml index 6b9873bf75..00b111b264 100644 --- a/apollo-router-scaffold/templates/base/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/Cargo.template.toml @@ -22,7 +22,7 @@ apollo-router = { path ="{{integration_test}}apollo-router" } apollo-router = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} # Note if you update these dependencies then also update xtask/Cargo.toml -apollo-router = "1.57.1" +apollo-router = "1.58.0" {{/if}} {{/if}} async-trait = "0.1.52" diff --git a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml index f58bd86237..db2a939c2a 100644 --- a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml @@ -13,7 +13,7 @@ apollo-router-scaffold = { path ="{{integration_test}}apollo-router-scaffold" } {{#if branch}} apollo-router-scaffold = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} -apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.57.1" } +apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.58.0" } {{/if}} {{/if}} anyhow = "1.0.58" diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index ced84ad29f..f2cedca917 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router" -version = "1.57.1" +version = "1.58.0" authors = ["Apollo Graph, Inc. "] repository = "https://github.com/apollographql/router/" documentation = "https://docs.rs/apollo-router" @@ -52,6 +52,10 @@ docs_rs = ["router-bridge/docs_rs"] # and not yet ready for production use. telemetry_next = [] +# Allow Router to use feature from custom fork of Hyper until it is merged: +# https://github.com/hyperium/hyper/pull/3523 +hyper_header_limits = [] + # is set when ci builds take place. It allows us to disable some tests when CI is running on certain platforms. ci = [] @@ -62,7 +66,7 @@ features = ["docs_rs"] access-json = "0.1.0" anyhow = "1.0.86" apollo-compiler.workspace = true -apollo-federation = { path = "../apollo-federation", version = "=1.57.1" } +apollo-federation = { path = "../apollo-federation", version = "=1.58.0" } arc-swap = "1.6.0" async-channel = "1.9.0" async-compression = { version = "0.4.6", features = [ @@ -105,7 +109,7 @@ http-body = "0.4.6" heck = "0.5.0" humantime = "2.1.0" humantime-serde = "1.1.1" -hyper = { version = "0.14.28", features = ["server", "client", "stream"] } +hyper = { version = "0.14.31", features = ["server", "client", "stream"] } hyper-rustls = { version = "0.24.2", features = ["http1", "http2"] } indexmap = { version = "2.2.6", features = ["serde"] } itertools = "0.13.0" diff --git a/apollo-router/src/ageing_priority_queue.rs b/apollo-router/src/ageing_priority_queue.rs new file mode 100644 index 0000000000..d206283093 --- /dev/null +++ b/apollo-router/src/ageing_priority_queue.rs @@ -0,0 +1,147 @@ +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; + +/// Items with higher priority value get handled sooner +#[allow(unused)] +pub(crate) enum Priority { + P1 = 1, + P2, + P3, + P4, + P5, + P6, + P7, + P8, +} + +const INNER_QUEUES_COUNT: usize = Priority::P8 as usize - Priority::P1 as usize + 1; + +/// Indices start at 0 for highest priority +const fn index_from_priority(priority: Priority) -> usize { + Priority::P8 as usize - priority as usize +} + +const _: () = { + assert!(index_from_priority(Priority::P1) == 7); + assert!(index_from_priority(Priority::P8) == 0); +}; + +pub(crate) struct AgeingPriorityQueue +where + T: Send + 'static, +{ + /// Items in **lower** indices queues are handled sooner + inner_queues: + [(crossbeam_channel::Sender, crossbeam_channel::Receiver); INNER_QUEUES_COUNT], + queued_count: AtomicUsize, + soft_capacity: usize, +} + +pub(crate) struct Receiver<'a, T> +where + T: Send + 'static, +{ + shared: &'a AgeingPriorityQueue, + select: crossbeam_channel::Select<'a>, +} + +impl AgeingPriorityQueue +where + T: Send + 'static, +{ + pub(crate) fn soft_bounded(soft_capacity: usize) -> Self { + Self { + // Using unbounded channels: callers must use `is_full` to implement backpressure + inner_queues: std::array::from_fn(|_| crossbeam_channel::unbounded()), + queued_count: AtomicUsize::new(0), + soft_capacity, + } + } + + pub(crate) fn queued_count(&self) -> usize { + self.queued_count.load(Ordering::Relaxed) + } + + pub(crate) fn is_full(&self) -> bool { + self.queued_count() >= self.soft_capacity + } + + /// Panics if `priority` is not in `AVAILABLE_PRIORITIES` + pub(crate) fn send(&self, priority: Priority, message: T) { + self.queued_count.fetch_add(1, Ordering::Relaxed); + let (inner_sender, _) = &self.inner_queues[index_from_priority(priority)]; + inner_sender.send(message).expect("disconnected channel") + } + + pub(crate) fn receiver(&self) -> Receiver<'_, T> { + let mut select = crossbeam_channel::Select::new(); + for (_, inner_receiver) in &self.inner_queues { + select.recv(inner_receiver); + } + Receiver { + shared: self, + select, + } + } +} + +impl<'a, T> Receiver<'a, T> +where + T: Send + 'static, +{ + pub(crate) fn blocking_recv(&mut self) -> T { + loop { + // Block until something is ready. + // Ignore the returned index because it is "random" when multiple operations are ready. + self.select.ready(); + // Check inner channels in priority order instead: + for (index, (_, inner_receiver)) in self.shared.inner_queues.iter().enumerate() { + if let Ok(message) = inner_receiver.try_recv() { + self.shared.queued_count.fetch_sub(1, Ordering::Relaxed); + self.age(index); + return message; + } + } + // Another thread raced us to it or `ready()` returned spuriously, try again + } + } + + // Promote some messages from priorities lower (higher indices) than `message_consumed_at_index` + fn age(&self, message_consumed_at_index: usize) { + for window in self.shared.inner_queues[message_consumed_at_index..].windows(2) { + let [higher_priority, lower_priority] = window else { + panic!("expected windows of length 2") + }; + let (higher_priority_sender, _) = higher_priority; + let (_, lower_priority_receiver) = lower_priority; + if let Ok(message) = lower_priority_receiver.try_recv() { + higher_priority_sender + .send(message) + .expect("disconnected channel") + } + } + } +} + +#[test] +fn test_priorities() { + let queue = AgeingPriorityQueue::soft_bounded(3); + assert_eq!(queue.queued_count(), 0); + assert!(!queue.is_full()); + queue.send(Priority::P1, "p1"); + assert!(!queue.is_full()); + queue.send(Priority::P2, "p2"); + assert!(!queue.is_full()); + queue.send(Priority::P3, "p3"); + // The queue is now "full" but sending still works, it’s up to the caller to stop sending + assert!(queue.is_full()); + queue.send(Priority::P2, "p2 again"); + assert_eq!(queue.queued_count(), 4); + + let mut receiver = queue.receiver(); + assert_eq!(receiver.blocking_recv(), "p3"); + assert_eq!(receiver.blocking_recv(), "p2"); + assert_eq!(receiver.blocking_recv(), "p2 again"); + assert_eq!(receiver.blocking_recv(), "p1"); + assert_eq!(queue.queued_count(), 0); +} diff --git a/apollo-router/src/axum_factory/axum_http_server_factory.rs b/apollo-router/src/axum_factory/axum_http_server_factory.rs index 08df933dc6..391424f0b1 100644 --- a/apollo-router/src/axum_factory/axum_http_server_factory.rs +++ b/apollo-router/src/axum_factory/axum_http_server_factory.rs @@ -5,6 +5,7 @@ use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; use std::sync::Arc; +use std::time::Duration; use std::time::Instant; use axum::error_handling::HandleErrorLayer; @@ -24,6 +25,7 @@ use http::header::CONTENT_ENCODING; use http::HeaderValue; use http::Request; use http_body::combinators::UnsyncBoxBody; +use hyper::server::conn::Http; use hyper::Body; use itertools::Itertools; use multimap::MultiMap; @@ -298,12 +300,25 @@ impl HttpServerFactory for AxumHttpServerFactory { let actual_main_listen_address = main_listener .local_addr() .map_err(ApolloRouterError::ServerCreationError)?; + let mut http_config = Http::new(); + http_config.http1_keep_alive(true); + http_config.http1_header_read_timeout(Duration::from_secs(10)); + + #[cfg(feature = "hyper_header_limits")] + if let Some(max_headers) = configuration.limits.http1_max_request_headers { + http_config.http1_max_headers(max_headers); + } + + if let Some(max_buf_size) = configuration.limits.http1_max_request_buf_size { + http_config.max_buf_size(max_buf_size.as_u64() as usize); + } let (main_server, main_shutdown_sender) = serve_router_on_listen_addr( main_listener, actual_main_listen_address.clone(), all_routers.main.1, true, + http_config.clone(), all_connections_stopped_sender.clone(), ); @@ -343,6 +358,7 @@ impl HttpServerFactory for AxumHttpServerFactory { listen_addr.clone(), router, false, + http_config.clone(), all_connections_stopped_sender.clone(), ); ( diff --git a/apollo-router/src/axum_factory/listeners.rs b/apollo-router/src/axum_factory/listeners.rs index dad439317c..52ea352979 100644 --- a/apollo-router/src/axum_factory/listeners.rs +++ b/apollo-router/src/axum_factory/listeners.rs @@ -202,6 +202,7 @@ pub(super) fn serve_router_on_listen_addr( address: ListenAddr, router: axum::Router, main_graphql_port: bool, + http_config: Http, all_connections_stopped_sender: mpsc::Sender<()>, ) -> (impl Future, oneshot::Sender<()>) { let (shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); @@ -243,6 +244,7 @@ pub(super) fn serve_router_on_listen_addr( } let address = address.clone(); + let mut http_config = http_config.clone(); tokio::task::spawn(async move { // this sender must be moved into the session to track that it is still running let _connection_stop_signal = connection_stop_signal; @@ -261,11 +263,8 @@ pub(super) fn serve_router_on_listen_addr( .expect( "this should not fail unless the socket is invalid", ); - let connection = Http::new() - .http1_keep_alive(true) - .http1_header_read_timeout(Duration::from_secs(10)) - .serve_connection(stream, app); + let connection = http_config.serve_connection(stream, app); tokio::pin!(connection); tokio::select! { // the connection finished first @@ -291,9 +290,7 @@ pub(super) fn serve_router_on_listen_addr( NetworkStream::Unix(stream) => { let received_first_request = Arc::new(AtomicBool::new(false)); let app = IdleConnectionChecker::new(received_first_request.clone(), app); - let connection = Http::new() - .http1_keep_alive(true) - .serve_connection(stream, app); + let connection = http_config.serve_connection(stream, app); tokio::pin!(connection); tokio::select! { @@ -329,9 +326,7 @@ pub(super) fn serve_router_on_listen_addr( let protocol = stream.get_ref().1.alpn_protocol(); let http2 = protocol == Some(&b"h2"[..]); - let connection = Http::new() - .http1_keep_alive(true) - .http1_header_read_timeout(Duration::from_secs(10)) + let connection = http_config .http2_only(http2) .serve_connection(stream, app); diff --git a/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap b/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap index c08d3d1693..6ae04241f1 100644 --- a/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap +++ b/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap @@ -20,13 +20,14 @@ expression: parts }, "errors": [ { - "message": "couldn't find mock for query {\"query\":\"query($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { reviews { __typename id product { __typename upc } } } } }\",\"variables\":{\"representations\":[{\"__typename\":\"Product\",\"upc\":\"1\"},{\"__typename\":\"Product\",\"upc\":\"2\"}]}}", + "message": "couldn't find mock for query {\"query\":\"query($representations: [_Any!]!) { _entities(representations: $representations) { ..._generated_onProduct1_0 } } fragment _generated_onProduct1_0 on Product { reviews { __typename id product { __typename upc } } }\",\"variables\":{\"representations\":[{\"__typename\":\"Product\",\"upc\":\"1\"},{\"__typename\":\"Product\",\"upc\":\"2\"}]}}", "path": [ "topProducts", "@" ], "extensions": { - "code": "FETCH_ERROR" + "code": "FETCH_ERROR", + "service": "reviews" } } ], diff --git a/apollo-router/src/axum_factory/tests.rs b/apollo-router/src/axum_factory/tests.rs index 87198b8f4f..efcf7e3ab3 100644 --- a/apollo-router/src/axum_factory/tests.rs +++ b/apollo-router/src/axum_factory/tests.rs @@ -1847,7 +1847,7 @@ async fn http_compressed_service() -> impl Service< "apollo.include_subgraph_errors": { "all": true } - } + }, })) .unwrap() .supergraph_hook(move |service| { diff --git a/apollo-router/src/batching.rs b/apollo-router/src/batching.rs index b570587b6f..a66aca8d87 100644 --- a/apollo-router/src/batching.rs +++ b/apollo-router/src/batching.rs @@ -28,6 +28,7 @@ use crate::services::http::HttpClientServiceFactory; use crate::services::process_batches; use crate::services::router::body::get_body_bytes; use crate::services::router::body::RouterBody; +use crate::services::subgraph::SubgraphRequestId; use crate::services::SubgraphRequest; use crate::services::SubgraphResponse; use crate::Context; @@ -426,7 +427,7 @@ pub(crate) async fn assemble_batch( ) -> Result< ( String, - Vec, + Vec<(Context, SubgraphRequestId)>, http::Request, Vec>>, ), @@ -445,8 +446,8 @@ pub(crate) async fn assemble_batch( // Retain the various contexts for later use let contexts = requests .iter() - .map(|x| x.context.clone()) - .collect::>(); + .map(|request| (request.context.clone(), request.id.clone())) + .collect::>(); // Grab the common info from the first request let first_request = requests .into_iter() @@ -470,19 +471,30 @@ mod tests { use std::sync::Arc; use std::time::Duration; + use http::header::ACCEPT; + use http::header::CONTENT_TYPE; use tokio::sync::oneshot; + use tower::ServiceExt; + use wiremock::matchers; + use wiremock::MockServer; + use wiremock::ResponseTemplate; use super::assemble_batch; use super::Batch; use super::BatchQueryInfo; use crate::graphql; - use crate::plugins::traffic_shaping::Http2Config; + use crate::graphql::Request; + use crate::layers::ServiceExt as LayerExt; use crate::query_planner::fetch::QueryHash; use crate::services::http::HttpClientServiceFactory; + use crate::services::router; + use crate::services::subgraph; + use crate::services::subgraph::SubgraphRequestId; use crate::services::SubgraphRequest; use crate::services::SubgraphResponse; use crate::Configuration; use crate::Context; + use crate::TestHarness; #[tokio::test(flavor = "multi_thread")] async fn it_assembles_batch() { @@ -523,7 +535,7 @@ mod tests { let output_context_ids = contexts .iter() - .map(|r| r.id.clone()) + .map(|r| r.0.id.clone()) .collect::>(); // Make sure all of our contexts are preserved during assembly assert_eq!(input_context_ids, output_context_ids); @@ -562,6 +574,7 @@ mod tests { .unwrap(), context: Context::new(), subgraph_name: None, + id: SubgraphRequestId(String::new()), }; tx.send(Ok(response)).unwrap(); @@ -620,7 +633,7 @@ mod tests { let factory = HttpClientServiceFactory::from_config( "testbatch", &Configuration::default(), - Http2Config::Disable, + crate::configuration::shared::Client::default(), ); let request = SubgraphRequest::fake_builder() .subgraph_request( @@ -659,7 +672,7 @@ mod tests { let factory = HttpClientServiceFactory::from_config( "testbatch", &Configuration::default(), - Http2Config::Disable, + crate::configuration::shared::Client::default(), ); let request = SubgraphRequest::fake_builder() .subgraph_request( @@ -694,7 +707,7 @@ mod tests { let factory = HttpClientServiceFactory::from_config( "testbatch", &Configuration::default(), - Http2Config::Disable, + crate::configuration::shared::Client::default(), ); let request = SubgraphRequest::fake_builder() .subgraph_request( @@ -722,4 +735,130 @@ mod tests { .await .is_err()); } + + fn expect_batch(request: &wiremock::Request) -> ResponseTemplate { + let requests: Vec = request.body_json().unwrap(); + + // Extract info about this operation + let (subgraph, count): (String, usize) = { + let re = regex::Regex::new(r"entry([AB])\(count:([0-9]+)\)").unwrap(); + let captures = re.captures(requests[0].query.as_ref().unwrap()).unwrap(); + + (captures[1].to_string(), captures[2].parse().unwrap()) + }; + + // We should have gotten `count` elements + assert_eq!(requests.len(), count); + + // Each element should have be for the specified subgraph and should have a field selection + // of index. + // Note: The router appends info to the query, so we append it at this check + for (index, request) in requests.into_iter().enumerate() { + assert_eq!( + request.query, + Some(format!( + "query op{index}__{}__0{{entry{}(count:{count}){{index}}}}", + subgraph.to_lowercase(), + subgraph + )) + ); + } + + ResponseTemplate::new(200).set_body_json( + (0..count) + .map(|index| { + serde_json::json!({ + "data": { + format!("entry{subgraph}"): { + "index": index + } + } + }) + }) + .collect::>(), + ) + } + + #[tokio::test(flavor = "multi_thread")] + async fn it_matches_subgraph_request_ids_to_responses() { + // Create a wiremock server for each handler + let mock_server = MockServer::start().await; + mock_server + .register( + wiremock::Mock::given(matchers::method("POST")) + .and(matchers::path("/a")) + .respond_with(expect_batch) + .expect(1), + ) + .await; + + let schema = include_str!("../tests/fixtures/batching/schema.graphql"); + let service = TestHarness::builder() + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { + "all": true + }, + "batching": { + "enabled": true, + "mode": "batch_http_link", + "subgraph": { + "all": { + "enabled": true + } + } + }, + "override_subgraph_url": { + "a": format!("{}/a", mock_server.uri()) + }})) + .unwrap() + .schema(schema) + .subgraph_hook(move |_subgraph_name, service| { + service + .map_future_with_request_data( + |r: &subgraph::Request| r.id.clone(), + |id, f| async move { + let r: subgraph::ServiceResult = f.await; + assert_eq!(id, r.as_ref().map(|r| r.id.clone()).unwrap()); + r + }, + ) + .boxed() + }) + .with_subgraph_network_requests() + .build_router() + .await + .unwrap(); + + let requests: Vec<_> = (0..3) + .map(|index| { + Request::fake_builder() + .query(format!("query op{index}{{ entryA(count: 3) {{ index }} }}")) + .build() + }) + .collect(); + let request = serde_json::to_value(requests).unwrap(); + + let context = Context::new(); + let request = router::Request { + context, + router_request: http::Request::builder() + .method("POST") + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .body(serde_json::to_vec(&request).unwrap().into()) + .unwrap(), + }; + + let response = service + .oneshot(request) + .await + .unwrap() + .next_response() + .await + .unwrap() + .unwrap(); + + let response: serde_json::Value = serde_json::from_slice(&response).unwrap(); + insta::assert_json_snapshot!(response); + } } diff --git a/apollo-router/src/compute_job.rs b/apollo-router/src/compute_job.rs new file mode 100644 index 0000000000..64fe16b20d --- /dev/null +++ b/apollo-router/src/compute_job.rs @@ -0,0 +1,120 @@ +use std::future::Future; +use std::panic::UnwindSafe; +use std::sync::OnceLock; + +use opentelemetry::metrics::MeterProvider as _; +use opentelemetry::metrics::ObservableGauge; +use tokio::sync::oneshot; + +use crate::ageing_priority_queue::AgeingPriorityQueue; +pub(crate) use crate::ageing_priority_queue::Priority; +use crate::metrics::meter_provider; + +/// We generate backpressure in tower `poll_ready` when the number of queued jobs +/// reaches `QUEUE_SOFT_CAPACITY_PER_THREAD * thread_pool_size()` +const QUEUE_SOFT_CAPACITY_PER_THREAD: usize = 20; + +/// Let this thread pool use all available resources if it can. +/// In the worst case, we’ll have moderate context switching cost +/// as the kernel’s scheduler distributes time to it or Tokio or other threads. +fn thread_pool_size() -> usize { + std::thread::available_parallelism() + .expect("available_parallelism() failed") + .get() +} + +type Job = Box; + +fn queue() -> &'static AgeingPriorityQueue { + static QUEUE: OnceLock> = OnceLock::new(); + QUEUE.get_or_init(|| { + let pool_size = thread_pool_size(); + for _ in 0..pool_size { + std::thread::spawn(|| { + // This looks like we need the queue before creating the queue, + // but it happens in a child thread where OnceLock will block + // until `get_or_init` in the parent thread is finished + // and the parent is *not* blocked on the child thread making progress. + let queue = queue(); + + let mut receiver = queue.receiver(); + loop { + let job = receiver.blocking_recv(); + job(); + } + }); + } + AgeingPriorityQueue::soft_bounded(QUEUE_SOFT_CAPACITY_PER_THREAD * pool_size) + }) +} + +/// Returns a future that resolves to a `Result` that is `Ok` if `f` returned or `Err` if it panicked. +pub(crate) fn execute( + priority: Priority, + job: F, +) -> impl Future> +where + F: FnOnce() -> T + Send + UnwindSafe + 'static, + T: Send + 'static, +{ + let (tx, rx) = oneshot::channel(); + let job = Box::new(move || { + // Ignore the error if the oneshot receiver was dropped + let _ = tx.send(std::panic::catch_unwind(job)); + }); + queue().send(priority, job); + async { rx.await.expect("channel disconnected") } +} + +pub(crate) fn is_full() -> bool { + queue().is_full() +} + +pub(crate) fn create_queue_size_gauge() -> ObservableGauge { + meter_provider() + .meter("apollo/router") + .u64_observable_gauge("apollo.router.compute_jobs.queued") + .with_description( + "Number of computation jobs (parsing, planning, …) waiting to be scheduled", + ) + .with_callback(move |m| m.observe(queue().queued_count() as u64, &[])) + .init() +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + use std::time::Instant; + + use super::*; + + #[tokio::test] + async fn test_executes_on_different_thread() { + let test_thread = std::thread::current().id(); + let job_thread = execute(Priority::P4, || std::thread::current().id()) + .await + .unwrap(); + assert_ne!(job_thread, test_thread) + } + + #[tokio::test] + async fn test_parallelism() { + if thread_pool_size() < 2 { + return; + } + let start = Instant::now(); + let one = execute(Priority::P8, || { + std::thread::sleep(Duration::from_millis(1_000)); + 1 + }); + let two = execute(Priority::P8, || { + std::thread::sleep(Duration::from_millis(1_000)); + 1 + 1 + }); + tokio::time::sleep(Duration::from_millis(500)).await; + assert_eq!(one.await.unwrap(), 1); + assert_eq!(two.await.unwrap(), 2); + // Evidence of fearless parallel sleep: + assert!(start.elapsed() < Duration::from_millis(1_400)); + } +} diff --git a/apollo-router/src/configuration/metrics.rs b/apollo-router/src/configuration/metrics.rs index 0020b20935..6ea8121a69 100644 --- a/apollo-router/src/configuration/metrics.rs +++ b/apollo-router/src/configuration/metrics.rs @@ -49,6 +49,8 @@ impl Metrics { data.populate_user_plugins_instrument(configuration); data.populate_query_planner_experimental_parallelism(configuration); data.populate_deno_or_rust_mode_instruments(configuration); + data.populate_legacy_fragment_usage(configuration); + data.into() } } @@ -316,7 +318,9 @@ impl InstrumentData { opt.subgraph.enabled, "$[?(@.subgraph.subgraphs..enabled)]", opt.subgraph.ttl, - "$[?(@.subgraph.all.ttl || @.subgraph.subgraphs..ttl)]" + "$[?(@.subgraph.all.ttl || @.subgraph.subgraphs..ttl)]", + opt.subgraph.invalidation.enabled, + "$[?(@.subgraph.all.invalidation.enabled || @.subgraph.subgraphs..invalidation.enabled)]" ); populate_config_instrument!( apollo.router.config.telemetry, @@ -494,6 +498,18 @@ impl InstrumentData { ); } + pub(crate) fn populate_legacy_fragment_usage(&mut self, configuration: &Configuration) { + // Fragment generation takes precedence over fragment reuse. Only report when fragment reuse is *actually active*. + if configuration.supergraph.reuse_query_fragments == Some(true) + && !configuration.supergraph.generate_query_fragments + { + self.data.insert( + "apollo.router.config.reuse_query_fragments".to_string(), + (1, HashMap::new()), + ); + } + } + pub(crate) fn populate_query_planner_experimental_parallelism( &mut self, configuration: &Configuration, diff --git a/apollo-router/src/configuration/mod.rs b/apollo-router/src/configuration/mod.rs index 00657cc11d..2df5ad8c62 100644 --- a/apollo-router/src/configuration/mod.rs +++ b/apollo-router/src/configuration/mod.rs @@ -412,6 +412,22 @@ impl Configuration { type_conditioned_fetching: self.experimental_type_conditioned_fetching, } } + + pub(crate) fn rust_query_planner_config( + &self, + ) -> apollo_federation::query_plan::query_planner::QueryPlannerConfig { + apollo_federation::query_plan::query_planner::QueryPlannerConfig { + reuse_query_fragments: self.supergraph.reuse_query_fragments.unwrap_or(true), + subgraph_graphql_validation: false, + generate_query_fragments: self.supergraph.generate_query_fragments, + incremental_delivery: + apollo_federation::query_plan::query_planner::QueryPlanIncrementalDeliveryConfig { + enable_defer: self.supergraph.defer_support, + }, + type_conditioned_fetching: self.experimental_type_conditioned_fetching, + debug: Default::default(), + } + } } impl Default for Configuration { @@ -476,6 +492,14 @@ impl Configuration { impl Configuration { pub(crate) fn validate(self) -> Result { + #[cfg(not(feature = "hyper_header_limits"))] + if self.limits.http1_max_request_headers.is_some() { + return Err(ConfigurationError::InvalidConfiguration { + message: "'limits.http1_max_request_headers' requires 'hyper_header_limits' feature", + error: "enable 'hyper_header_limits' feature in order to use 'limits.http1_max_request_headers'".to_string(), + }); + } + // Sandbox and Homepage cannot be both enabled if self.sandbox.enabled && self.homepage.enabled { return Err(ConfigurationError::InvalidConfiguration { @@ -665,7 +689,7 @@ pub(crate) struct Supergraph { pub(crate) reuse_query_fragments: Option, /// Enable QP generation of fragments for subgraph requests - /// Default: false + /// Default: true pub(crate) generate_query_fragments: bool, /// Set to false to disable defer support @@ -685,6 +709,10 @@ pub(crate) struct Supergraph { pub(crate) experimental_log_on_broken_pipe: bool, } +const fn default_generate_query_fragments() -> bool { + true +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "snake_case", untagged)] pub(crate) enum AvailableParallelism { @@ -737,7 +765,7 @@ impl Supergraph { Some(false) } else { reuse_query_fragments } ), - generate_query_fragments: generate_query_fragments.unwrap_or_default(), + generate_query_fragments: generate_query_fragments.unwrap_or_else(default_generate_query_fragments), early_cancel: early_cancel.unwrap_or_default(), experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(), } @@ -774,7 +802,7 @@ impl Supergraph { Some(false) } else { reuse_query_fragments } ), - generate_query_fragments: generate_query_fragments.unwrap_or_default(), + generate_query_fragments: generate_query_fragments.unwrap_or_else(default_generate_query_fragments), early_cancel: early_cancel.unwrap_or_default(), experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(), } diff --git a/apollo-router/src/configuration/shared/mod.rs b/apollo-router/src/configuration/shared/mod.rs index 9186b65a3d..ab164ec13f 100644 --- a/apollo-router/src/configuration/shared/mod.rs +++ b/apollo-router/src/configuration/shared/mod.rs @@ -3,8 +3,25 @@ use serde::Deserialize; use crate::plugins::traffic_shaping::Http2Config; -#[derive(Debug, Clone, Default, Deserialize, JsonSchema)] +#[derive(PartialEq, Debug, Clone, Default, Deserialize, JsonSchema, buildstructor::Builder)] #[serde(deny_unknown_fields, default)] pub(crate) struct Client { pub(crate) experimental_http2: Option, + pub(crate) dns_resolution_strategy: Option, +} + +#[derive(PartialEq, Default, Debug, Clone, Copy, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum DnsResolutionStrategy { + /// Only query for `A` (IPv4) records + Ipv4Only, + /// Only query for `AAAA` (IPv6) records + Ipv6Only, + /// Query for both `A` (IPv4) and `AAAA` (IPv6) records in parallel + Ipv4AndIpv6, + /// Query for `AAAA` (IPv6) records first; if that fails, query for `A` (IPv4) records + Ipv6ThenIpv4, + #[default] + /// Default: Query for `A` (IPv4) records first; if that fails, query for `AAAA` (IPv6) records + Ipv4ThenIpv6, } diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@entities.router.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@entities.router.yaml.snap index 9b586a2b76..1285a6de53 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@entities.router.yaml.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@entities.router.yaml.snap @@ -9,4 +9,5 @@ expression: "&metrics.non_zero()" attributes: opt.enabled: true opt.subgraph.enabled: true + opt.subgraph.invalidation.enabled: true opt.subgraph.ttl: true diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index a1939bc42a..0dc3c12eab 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -1,6 +1,7 @@ --- source: apollo-router/src/configuration/tests.rs expression: "&schema" +snapshot_kind: text --- { "$schema": "http://json-schema.org/draft-07/schema#", @@ -549,6 +550,11 @@ expression: "&schema" "Client": { "additionalProperties": false, "properties": { + "dns_resolution_strategy": { + "$ref": "#/definitions/DnsResolutionStrategy", + "description": "#/definitions/DnsResolutionStrategy", + "nullable": true + }, "experimental_http2": { "$ref": "#/definitions/Http2Config", "description": "#/definitions/Http2Config", @@ -1312,6 +1318,20 @@ expression: "&schema" "additionalProperties": false, "description": "Configuration for operation limits, parser limits, HTTP limits, etc.", "properties": { + "http1_max_request_buf_size": { + "default": null, + "description": "Limit the maximum buffer size for the HTTP1 connection.\n\nDefault is ~400kib.", + "nullable": true, + "type": "string" + }, + "http1_max_request_headers": { + "default": null, + "description": "Limit the maximum number of headers of incoming HTTP1 requests. Default is 100.\n\nIf router receives more headers than the buffer size, it responds to the client with \"431 Request Header Fields Too Large\".", + "format": "uint", + "minimum": 0.0, + "nullable": true, + "type": "integer" + }, "http_max_request_bytes": { "default": 2000000, "description": "Limit the size of incoming HTTP requests read from the network, to protect against running out of memory. Default: 2000000 (2 MB)", @@ -1681,6 +1701,11 @@ expression: "&schema" "description": "Enable or disable the entity caching feature", "type": "boolean" }, + "expose_keys_in_context": { + "default": false, + "description": "Expose cache keys in context", + "type": "boolean" + }, "invalidation": { "$ref": "#/definitions/InvalidationEndpointConfig", "description": "#/definitions/InvalidationEndpointConfig", @@ -2163,6 +2188,45 @@ expression: "&schema" } ] }, + "DnsResolutionStrategy": { + "oneOf": [ + { + "description": "Only query for `A` (IPv4) records", + "enum": [ + "ipv4_only" + ], + "type": "string" + }, + { + "description": "Only query for `AAAA` (IPv6) records", + "enum": [ + "ipv6_only" + ], + "type": "string" + }, + { + "description": "Query for both `A` (IPv4) and `AAAA` (IPv6) records in parallel", + "enum": [ + "ipv4_and_ipv6" + ], + "type": "string" + }, + { + "description": "Query for `AAAA` (IPv6) records first; if that fails, query for `A` (IPv4) records", + "enum": [ + "ipv6_then_ipv4" + ], + "type": "string" + }, + { + "description": "Default: Query for `A` (IPv4) records first; if that fails, query for `AAAA` (IPv6) records", + "enum": [ + "ipv4_then_ipv6" + ], + "type": "string" + } + ] + }, "Enabled": { "enum": [ "enabled" @@ -5930,6 +5994,11 @@ expression: "&schema" "description": "Send the service name", "type": "boolean" }, + "subgraph_request_id": { + "default": false, + "description": "Send the subgraph request id", + "type": "boolean" + }, "uri": { "default": false, "description": "Send the subgraph URI", @@ -5971,6 +6040,11 @@ expression: "&schema" "default": false, "description": "Send the http status", "type": "boolean" + }, + "subgraph_request_id": { + "default": false, + "description": "Send the subgraph request id", + "type": "boolean" } }, "type": "object" @@ -6395,6 +6469,11 @@ expression: "&schema" "nullable": true, "type": "boolean" }, + "dns_resolution_strategy": { + "$ref": "#/definitions/DnsResolutionStrategy", + "description": "#/definitions/DnsResolutionStrategy", + "nullable": true + }, "experimental_http2": { "$ref": "#/definitions/Http2Config", "description": "#/definitions/Http2Config", @@ -6545,8 +6624,8 @@ expression: "&schema" "type": "boolean" }, "generate_query_fragments": { - "default": false, - "description": "Enable QP generation of fragments for subgraph requests Default: false", + "default": true, + "description": "Enable QP generation of fragments for subgraph requests Default: true", "type": "boolean" }, "introspection": { diff --git a/apollo-router/src/configuration/testdata/metrics/entities.router.yaml b/apollo-router/src/configuration/testdata/metrics/entities.router.yaml index e2cbd0ee04..2382453c33 100644 --- a/apollo-router/src/configuration/testdata/metrics/entities.router.yaml +++ b/apollo-router/src/configuration/testdata/metrics/entities.router.yaml @@ -9,10 +9,15 @@ preview_entity_cache: urls: [ "redis://localhost:6379" ] timeout: 5ms ttl: 60s - enabled: true + invalidation: + enabled: true + shared_key: "invalidate" subgraphs: accounts: enabled: false products: - ttl: 120s \ No newline at end of file + ttl: 120s + invalidation: + enabled: true + shared_key: "invalidate" \ No newline at end of file diff --git a/apollo-router/src/configuration/tests.rs b/apollo-router/src/configuration/tests.rs index fb7acabf04..21cb5fdb50 100644 --- a/apollo-router/src/configuration/tests.rs +++ b/apollo-router/src/configuration/tests.rs @@ -413,6 +413,11 @@ fn validate_project_config_files() { }; for yaml in yamls { + #[cfg(not(feature = "hyper_header_limits"))] + if yaml.contains("http1_max_request_headers") { + continue; + } + if let Err(e) = validate_yaml_configuration( &yaml, Expansion::default().unwrap(), diff --git a/apollo-router/src/error.rs b/apollo-router/src/error.rs index 80b075a915..d78cd86728 100644 --- a/apollo-router/src/error.rs +++ b/apollo-router/src/error.rs @@ -1,4 +1,5 @@ //! Router errors. +use std::collections::HashMap; use std::sync::Arc; use apollo_compiler::validation::DiagnosticList; @@ -26,6 +27,11 @@ use crate::json_ext::Value; use crate::spec::operation_limits::OperationLimits; use crate::spec::SpecError; +/// Return up to this many GraphQL parsing or validation errors. +/// +/// Any remaining errors get silently dropped. +const MAX_VALIDATION_ERRORS: usize = 100; + /// Error types for execution. /// /// Note that these are not actually returned to the client, but are instead converted to JSON for @@ -333,6 +339,7 @@ impl IntoGraphQLErrors for Vec { .extension_code("GRAPHQL_VALIDATION_FAILED") .build() }) + .take(MAX_VALIDATION_ERRORS) .collect()) } } @@ -406,6 +413,19 @@ impl IntoGraphQLErrors for QueryPlannerError { } } +impl QueryPlannerError { + pub(crate) fn usage_reporting(&self) -> Option { + match self { + QueryPlannerError::PlanningErrors(pe) => Some(pe.usage_reporting.clone()), + QueryPlannerError::SpecError(e) => Some(UsageReporting { + stats_report_key: e.get_error_key().to_string(), + referenced_fields_by_type: HashMap::new(), + }), + _ => None, + } + } +} + #[derive(Clone, Debug, Error, Serialize, Deserialize)] /// Container for planner setup errors pub(crate) struct PlannerErrors(Arc>); @@ -605,6 +625,7 @@ impl IntoGraphQLErrors for ParseErrors { .extension_code("GRAPHQL_PARSING_FAILED") .build() }) + .take(MAX_VALIDATION_ERRORS) .collect()) } } @@ -635,6 +656,7 @@ impl ValidationErrors { .extension_code("GRAPHQL_VALIDATION_FAILED") .build() }) + .take(MAX_VALIDATION_ERRORS) .collect() } } @@ -647,7 +669,11 @@ impl IntoGraphQLErrors for ValidationErrors { impl From for ValidationErrors { fn from(errors: DiagnosticList) -> Self { Self { - errors: errors.iter().map(|e| e.unstable_to_json_compat()).collect(), + errors: errors + .iter() + .map(|e| e.unstable_to_json_compat()) + .take(MAX_VALIDATION_ERRORS) + .collect(), } } } diff --git a/apollo-router/src/graphql/visitor.rs b/apollo-router/src/graphql/visitor.rs index b344393821..aa901508db 100644 --- a/apollo-router/src/graphql/visitor.rs +++ b/apollo-router/src/graphql/visitor.rs @@ -92,18 +92,11 @@ pub(crate) trait ResponseVisitor { inner_field.as_ref(), value, ); - } else { - tracing::warn!("The response did not include a field corresponding to query field {:?}", inner_field); } } apollo_compiler::executable::Selection::FragmentSpread(fragment_spread) => { if let Some(fragment) = fragment_spread.fragment_def(request) { self.visit_selections(request, variables, &fragment.selection_set, fields); - } else { - tracing::warn!( - "The fragment {} was not found in the query document.", - fragment_spread.fragment_name - ); } } apollo_compiler::executable::Selection::InlineFragment(inline_fragment) => { diff --git a/apollo-router/src/introspection.rs b/apollo-router/src/introspection.rs index a0a539895d..b69b03c68b 100644 --- a/apollo-router/src/introspection.rs +++ b/apollo-router/src/introspection.rs @@ -6,6 +6,7 @@ use apollo_compiler::executable::Selection; use serde_json_bytes::json; use crate::cache::storage::CacheStorage; +use crate::compute_job; use crate::graphql; use crate::query_planner::QueryKey; use crate::services::layers::query_analysis::ParsedDocument; @@ -130,8 +131,9 @@ impl IntrospectionCache { } let schema = schema.clone(); let doc = doc.clone(); + let priority = compute_job::Priority::P1; // Low priority let response = - tokio::task::spawn_blocking(move || Self::execute_introspection(&schema, &doc)) + compute_job::execute(priority, move || Self::execute_introspection(&schema, &doc)) .await .expect("Introspection panicked"); storage.insert(cache_key, response.clone()).await; diff --git a/apollo-router/src/json_ext.rs b/apollo-router/src/json_ext.rs index c9e617635f..81423deb8b 100644 --- a/apollo-router/src/json_ext.rs +++ b/apollo-router/src/json_ext.rs @@ -72,6 +72,12 @@ pub(crate) trait ValueExt { #[track_caller] fn deep_merge(&mut self, other: Self); + /// Deep merge two JSON objects, overwriting values in `self` if it has the same key as `other`. + /// For GraphQL response objects, this uses schema information to avoid overwriting a concrete + /// `__typename` with an interface name. + #[track_caller] + fn type_aware_deep_merge(&mut self, other: Self, schema: &Schema); + /// Returns `true` if the values are equal and the objects are ordered the same. /// /// **Note:** this is recursive. @@ -146,8 +152,8 @@ pub(crate) trait ValueExt { #[track_caller] fn is_object_of_type(&self, schema: &Schema, maybe_type: &str) -> bool; - /// Convert this value to an instance of `apollo_compiler::ast::Value` - fn to_ast(&self) -> apollo_compiler::ast::Value; + /// value type + fn json_type_name(&self) -> &'static str; fn as_i32(&self) -> Option; } @@ -189,6 +195,53 @@ impl ValueExt for Value { } } + fn type_aware_deep_merge(&mut self, other: Self, schema: &Schema) { + match (self, other) { + (Value::Object(a), Value::Object(b)) => { + for (key, value) in b.into_iter() { + let k = key.clone(); + match a.entry(key) { + Entry::Vacant(e) => { + e.insert(value); + } + Entry::Occupied(e) => match (e.into_mut(), value) { + (Value::String(type1), Value::String(type2)) + if k.as_str() == TYPENAME => + { + // If type1 is a subtype of type2, we skip this overwrite to preserve the more specific `__typename` + // in the response. Ideally, we could use `Schema::is_implementation`, but that looks to be buggy + // and does not catch the problem we are trying to resolve. + if !schema.is_subtype(type2.as_str(), type1.as_str()) { + *type1 = type2; + } + } + (t, s) => t.type_aware_deep_merge(s, schema), + }, + } + } + } + (Value::Array(a), Value::Array(mut b)) => { + for (b_value, a_value) in b.drain(..min(a.len(), b.len())).zip(a.iter_mut()) { + a_value.type_aware_deep_merge(b_value, schema); + } + + a.extend(b); + } + (_, Value::Null) => {} + (Value::Object(_), Value::Array(_)) => { + failfast_debug!("trying to replace an object with an array"); + } + (Value::Array(_), Value::Object(_)) => { + failfast_debug!("trying to replace an array with an object"); + } + (a, b) => { + if b != Value::Null { + *a = b; + } + } + } + } + #[cfg(test)] fn eq_and_ordered(&self, other: &Self) -> bool { match (self, other) { @@ -475,34 +528,14 @@ impl ValueExt for Value { }) } - fn to_ast(&self) -> apollo_compiler::ast::Value { + fn json_type_name(&self) -> &'static str { match self { - Value::Null => apollo_compiler::ast::Value::Null, - Value::Bool(b) => apollo_compiler::ast::Value::Boolean(*b), - Value::Number(n) if n.is_f64() => { - apollo_compiler::ast::Value::Float(n.as_f64().expect("is float").into()) - } - Value::Number(n) => { - apollo_compiler::ast::Value::Int((n.as_i64().expect("is int") as i32).into()) - } - Value::String(s) => apollo_compiler::ast::Value::String(s.as_str().to_string()), - Value::Array(inner_vars) => apollo_compiler::ast::Value::List( - inner_vars - .iter() - .map(|v| apollo_compiler::Node::new(v.to_ast())) - .collect(), - ), - Value::Object(inner_vars) => apollo_compiler::ast::Value::Object( - inner_vars - .iter() - .map(|(k, v)| { - ( - apollo_compiler::Name::new(k.as_str()).expect("is valid name"), - apollo_compiler::Node::new(v.to_ast()), - ) - }) - .collect(), - ), + Value::Array(_) => "array", + Value::Null => "null", + Value::Bool(_) => "boolean", + Value::Number(_) => "number", + Value::String(_) => "string", + Value::Object(_) => "object", } } @@ -1319,6 +1352,66 @@ mod tests { ); } + #[test] + fn interface_typename_merging() { + let schema = Schema::parse( + r#" + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + directive @join__graph(name: String!, url: String!) on ENUM_VALUE + + scalar link__Import + scalar join__FieldSet + + enum link__Purpose { + SECURITY + EXECUTION + } + + enum join__Graph { + TEST @join__graph(name: "test", url: "http://localhost:4001/graphql") + } + + interface I { + s: String + } + + type C implements I { + s: String + } + + type Query { + i: I + } + "#, + &Default::default(), + ) + .expect("valid schema"); + let mut response1 = json!({ + "__typename": "C" + }); + let response2 = json!({ + "__typename": "I", + "s": "data" + }); + + response1.type_aware_deep_merge(response2, &schema); + + assert_eq!( + response1, + json!({ + "__typename": "C", + "s": "data" + }) + ); + } + #[test] fn test_is_subset_eq() { assert_is_subset!( diff --git a/apollo-router/src/lib.rs b/apollo-router/src/lib.rs index 9ac39c9e23..6f30ab6239 100644 --- a/apollo-router/src/lib.rs +++ b/apollo-router/src/lib.rs @@ -52,10 +52,12 @@ pub mod plugin; #[macro_use] pub(crate) mod metrics; +mod ageing_priority_queue; mod apollo_studio_interop; pub(crate) mod axum_factory; mod batching; mod cache; +mod compute_job; mod configuration; mod context; mod error; diff --git a/apollo-router/src/plugin/test/mock/canned.rs b/apollo-router/src/plugin/test/mock/canned.rs index 099f494f0c..369715444e 100644 --- a/apollo-router/src/plugin/test/mock/canned.rs +++ b/apollo-router/src/plugin/test/mock/canned.rs @@ -52,7 +52,7 @@ pub(crate) fn reviews_subgraph() -> MockSubgraph { let review_mocks = vec![ ( json! {{ - "query": "query TopProducts__reviews__1($representations:[_Any!]!){_entities(representations:$representations){...on Product{reviews{id product{__typename upc}author{__typename id}}}}}", + "query": "query TopProducts__reviews__1($representations:[_Any!]!){_entities(representations:$representations){..._generated_onProduct1_0}}fragment _generated_onProduct1_0 on Product{reviews{id product{__typename upc}author{__typename id}}}", "operationName": "TopProducts__reviews__1", "variables": { "representations":[ diff --git a/apollo-router/src/plugin/test/mock/subgraph.rs b/apollo-router/src/plugin/test/mock/subgraph.rs index 46321b585b..ee38234606 100644 --- a/apollo-router/src/plugin/test/mock/subgraph.rs +++ b/apollo-router/src/plugin/test/mock/subgraph.rs @@ -209,7 +209,12 @@ impl Service for MockSubgraph { let http_response = http_response_builder .body(response.clone()) .expect("Response is serializable; qed"); - SubgraphResponse::new_from_response(http_response, req.context, "test".to_string()) + SubgraphResponse::new_from_response( + http_response, + req.context, + "test".to_string(), + req.id, + ) } else { let error = crate::error::Error::builder() .message(format!( @@ -222,6 +227,7 @@ impl Service for MockSubgraph { SubgraphResponse::fake_builder() .error(error) .context(req.context) + .id(req.id) .build() }; future::ok(response) diff --git a/apollo-router/src/plugins/authentication/subgraph.rs b/apollo-router/src/plugins/authentication/subgraph.rs index 9dce2b1e45..568aa9c8ac 100644 --- a/apollo-router/src/plugins/authentication/subgraph.rs +++ b/apollo-router/src/plugins/authentication/subgraph.rs @@ -508,6 +508,7 @@ mod test { use crate::graphql::Request; use crate::plugin::test::MockSubgraphService; use crate::query_planner::fetch::OperationKind; + use crate::services::subgraph::SubgraphRequestId; use crate::services::SubgraphRequest; use crate::services::SubgraphResponse; use crate::Context; @@ -806,6 +807,7 @@ mod test { http::Response::default(), Context::new(), req.subgraph_name.unwrap_or_else(|| String::from("test")), + SubgraphRequestId(String::new()), )) } diff --git a/apollo-router/src/plugins/authorization/tests.rs b/apollo-router/src/plugins/authorization/tests.rs index d967125881..b8b1b8052b 100644 --- a/apollo-router/src/plugins/authorization/tests.rs +++ b/apollo-router/src/plugins/authorization/tests.rs @@ -22,7 +22,7 @@ async fn authenticated_request() { let subgraphs = MockedSubgraphs([ ("user", MockSubgraph::builder().with_json( serde_json::json!{{ - "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name phone}}}", + "query": "query($representations:[_Any!]!){_entities(representations:$representations){..._generated_onUser2_0}}fragment _generated_onUser2_0 on User{name phone}", "variables": { "representations": [ { "__typename": "User", "id":0 } diff --git a/apollo-router/src/plugins/cache/cache_control.rs b/apollo-router/src/plugins/cache/cache_control.rs index dd7d1a598e..4aa300e077 100644 --- a/apollo-router/src/plugins/cache/cache_control.rs +++ b/apollo-router/src/plugins/cache/cache_control.rs @@ -157,7 +157,24 @@ impl CacheControl { Ok(result) } + /// Fill the header map with cache-control header and age header pub(crate) fn to_headers(&self, headers: &mut HeaderMap) -> Result<(), BoxError> { + headers.insert( + CACHE_CONTROL, + HeaderValue::from_str(&self.to_cache_control_header()?)?, + ); + + if let Some(age) = self.age { + if age != 0 { + headers.insert(AGE, age.into()); + } + } + + Ok(()) + } + + /// Only for cache control header and not age + pub(crate) fn to_cache_control_header(&self) -> Result { let mut s = String::new(); let mut prev = false; let now = now_epoch_seconds(); @@ -228,15 +245,8 @@ impl CacheControl { if self.stale_if_error { write!(&mut s, "{}stale-if-error", if prev { "," } else { "" },)?; } - headers.insert(CACHE_CONTROL, HeaderValue::from_str(&s)?); - - if let Some(age) = self.age { - if age != 0 { - headers.insert(AGE, age.into()); - } - } - Ok(()) + Ok(s) } pub(super) fn no_store() -> Self { diff --git a/apollo-router/src/plugins/cache/entity.rs b/apollo-router/src/plugins/cache/entity.rs index b0abd0178e..d50531dde2 100644 --- a/apollo-router/src/plugins/cache/entity.rs +++ b/apollo-router/src/plugins/cache/entity.rs @@ -51,6 +51,7 @@ use crate::plugins::authorization::CacheKeyMetadata; use crate::query_planner::fetch::QueryHash; use crate::query_planner::OperationKind; use crate::services::subgraph; +use crate::services::subgraph::SubgraphRequestId; use crate::services::supergraph; use crate::spec::TYPENAME; use crate::Context; @@ -62,6 +63,8 @@ pub(crate) const ENTITY_CACHE_VERSION: &str = "1.0"; pub(crate) const ENTITIES: &str = "_entities"; pub(crate) const REPRESENTATIONS: &str = "representations"; pub(crate) const CONTEXT_CACHE_KEY: &str = "apollo_entity_cache::key"; +/// Context key to enable support of surrogate cache key +pub(crate) const CONTEXT_CACHE_KEYS: &str = "apollo::entity_cache::cached_keys_status"; register_plugin!("apollo", "preview_entity_cache", EntityCache); @@ -73,6 +76,7 @@ pub(crate) struct EntityCache { entity_type: Option, enabled: bool, metrics: Metrics, + expose_keys_in_context: bool, private_queries: Arc>>, pub(crate) invalidation: Invalidation, } @@ -96,6 +100,10 @@ pub(crate) struct Config { #[serde(default)] enabled: bool, + #[serde(default)] + /// Expose cache keys in context + expose_keys_in_context: bool, + /// Configure invalidation per subgraph subgraph: SubgraphConfiguration, @@ -297,6 +305,7 @@ impl Plugin for EntityCache { storage, entity_type, enabled: init.config.enabled, + expose_keys_in_context: init.config.expose_keys_in_context, endpoint_config: init.config.invalidation.clone().map(Arc::new), subgraphs: Arc::new(init.config.subgraph), metrics: init.config.metrics, @@ -390,6 +399,7 @@ impl Plugin for EntityCache { private_queries, private_id, invalidation: self.invalidation.clone(), + expose_keys_in_context: self.expose_keys_in_context, }))); tower::util::BoxService::new(inner) } else { @@ -467,6 +477,7 @@ impl EntityCache { storage, entity_type: None, enabled: true, + expose_keys_in_context: true, subgraphs: Arc::new(SubgraphConfiguration { all: Subgraph::default(), subgraphs, @@ -496,6 +507,7 @@ struct InnerCacheService { subgraph_ttl: Option, private_queries: Arc>>, private_id: Option, + expose_keys_in_context: bool, invalidation: Invalidation, } @@ -567,6 +579,7 @@ impl InnerCacheService { self.storage.clone(), is_known_private, private_id.as_deref(), + self.expose_keys_in_context, request, ) .instrument(tracing::info_span!("cache.entity.lookup")) @@ -614,6 +627,7 @@ impl InnerCacheService { if private_id.is_none() { // the response has a private scope but we don't have a way to differentiate users, so we do not store the response in cache + // We don't need to fill the context with this cache key as it will never be cached return Ok(response); } } @@ -638,6 +652,7 @@ impl InnerCacheService { &response, cache_control, root_cache_key, + self.expose_keys_in_context, ) .await?; } @@ -663,12 +678,14 @@ impl InnerCacheService { Ok(response) } } else { + let request_id = request.id.clone(); match cache_lookup_entities( self.name.clone(), self.storage.clone(), is_known_private, private_id.as_deref(), request, + self.expose_keys_in_context, ) .instrument(tracing::info_span!("cache.entity.lookup")) .await? @@ -701,6 +718,20 @@ impl InnerCacheService { &[graphql_error], &mut cache_result.0, ); + if self.expose_keys_in_context { + // Update cache keys needed for surrogate cache key because new data has not been fetched + context.upsert::<_, CacheKeysContext>( + CONTEXT_CACHE_KEYS, + |mut value| { + if let Some(cache_keys) = value.get_mut(&request_id) { + cache_keys.retain(|cache_key| { + matches!(cache_key.status, CacheKeyStatus::Cached) + }); + } + value + }, + )?; + } let mut data = Object::default(); data.insert(ENTITIES, new_entities.into()); @@ -727,6 +758,25 @@ impl InnerCacheService { if let Some(control_from_cached) = cache_result.1 { cache_control = cache_control.merge(&control_from_cached); } + if self.expose_keys_in_context { + // Update cache keys needed for surrogate cache key when it's new data and not data from the cache + let response_id = response.id.clone(); + let cache_control_str = cache_control.to_cache_control_header()?; + response.context.upsert::<_, CacheKeysContext>( + CONTEXT_CACHE_KEYS, + |mut value| { + if let Some(cache_keys) = value.get_mut(&response_id) { + for cache_key in cache_keys + .iter_mut() + .filter(|c| matches!(c.status, CacheKeyStatus::New)) + { + cache_key.cache_control = cache_control_str.clone(); + } + } + value + }, + )?; + } if !is_known_private && cache_control.private() { self.private_queries.write().await.insert(query.to_string()); @@ -797,6 +847,7 @@ async fn cache_lookup_root( cache: RedisCacheStorage, is_known_private: bool, private_id: Option<&str>, + expose_keys_in_context: bool, mut request: subgraph::Request, ) -> Result, BoxError> { let body = request.subgraph_request.body_mut(); @@ -822,6 +873,36 @@ async fn cache_lookup_root( .context .extensions() .with_lock(|mut lock| lock.insert(control)); + if expose_keys_in_context { + let request_id = request.id.clone(); + let cache_control_header = value.0.control.to_cache_control_header()?; + request.context.upsert::<_, CacheKeysContext>( + CONTEXT_CACHE_KEYS, + |mut val| { + match val.get_mut(&request_id) { + Some(v) => { + v.push(CacheKeyContext { + key: key.clone(), + status: CacheKeyStatus::Cached, + cache_control: cache_control_header, + }); + } + None => { + val.insert( + request_id, + vec![CacheKeyContext { + key: key.clone(), + status: CacheKeyStatus::Cached, + cache_control: cache_control_header, + }], + ); + } + } + + val + }, + )?; + } let mut response = subgraph::Response::builder() .data(value.0.data) @@ -851,6 +932,7 @@ async fn cache_lookup_entities( is_known_private: bool, private_id: Option<&str>, mut request: subgraph::Request, + expose_keys_in_context: bool, ) -> Result, BoxError> { let body = request.subgraph_request.body_mut(); @@ -893,6 +975,46 @@ async fn cache_lookup_entities( let (new_representations, cache_result, cache_control) = filter_representations(&name, representations, keys, cache_result, &request.context)?; + if expose_keys_in_context { + let mut cache_entries = Vec::with_capacity(cache_result.len()); + for intermediate_result in &cache_result { + match &intermediate_result.cache_entry { + Some(cache_entry) => { + cache_entries.push(CacheKeyContext { + key: intermediate_result.key.clone(), + status: CacheKeyStatus::Cached, + cache_control: cache_entry.control.to_cache_control_header()?, + }); + } + None => { + cache_entries.push(CacheKeyContext { + key: intermediate_result.key.clone(), + status: CacheKeyStatus::New, + cache_control: match &cache_control { + Some(cc) => cc.to_cache_control_header()?, + None => CacheControl::default().to_cache_control_header()?, + }, + }); + } + } + } + let request_id = request.id.clone(); + request + .context + .upsert::<_, CacheKeysContext>(CONTEXT_CACHE_KEYS, |mut v| { + match v.get_mut(&request_id) { + Some(cache_keys) => { + cache_keys.append(&mut cache_entries); + } + None => { + v.insert(request_id, cache_entries); + } + } + + v + })?; + } + if !new_representations.is_empty() { body.variables .insert(REPRESENTATIONS, new_representations.into()); @@ -954,6 +1076,7 @@ async fn cache_store_root_from_response( response: &subgraph::Response, cache_control: CacheControl, cache_key: String, + expose_keys_in_context: bool, ) -> Result<(), BoxError> { if let Some(data) = response.response.body().data.as_ref() { let ttl: Option = cache_control @@ -964,6 +1087,37 @@ async fn cache_store_root_from_response( if response.response.body().errors.is_empty() && cache_control.should_store() { let span = tracing::info_span!("cache.entity.store"); let data = data.clone(); + if expose_keys_in_context { + let response_id = response.id.clone(); + let cache_control_header = cache_control.to_cache_control_header()?; + + response + .context + .upsert::<_, CacheKeysContext>(CONTEXT_CACHE_KEYS, |mut val| { + match val.get_mut(&response_id) { + Some(v) => { + v.push(CacheKeyContext { + key: cache_key.clone(), + status: CacheKeyStatus::New, + cache_control: cache_control_header, + }); + } + None => { + val.insert( + response_id, + vec![CacheKeyContext { + key: cache_key.clone(), + status: CacheKeyStatus::New, + cache_control: cache_control_header, + }], + ); + } + } + + val + })?; + } + tokio::spawn(async move { cache .insert( @@ -1419,3 +1573,42 @@ fn assemble_response_from_errors( } (new_entities, new_errors) } + +pub(crate) type CacheKeysContext = HashMap>; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(PartialEq, Eq, Hash, PartialOrd, Ord))] +pub(crate) struct CacheKeyContext { + key: String, + status: CacheKeyStatus, + cache_control: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(PartialEq, Eq, Hash))] +#[serde(rename_all = "snake_case")] +pub(crate) enum CacheKeyStatus { + /// New cache key inserted in the cache + New, + /// Key that was already in the cache + Cached, +} + +#[cfg(test)] +impl PartialOrd for CacheKeyStatus { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[cfg(test)] +impl Ord for CacheKeyStatus { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (CacheKeyStatus::New, CacheKeyStatus::New) => std::cmp::Ordering::Equal, + (CacheKeyStatus::New, CacheKeyStatus::Cached) => std::cmp::Ordering::Greater, + (CacheKeyStatus::Cached, CacheKeyStatus::New) => std::cmp::Ordering::Less, + (CacheKeyStatus::Cached, CacheKeyStatus::Cached) => std::cmp::Ordering::Equal, + } + } +} diff --git a/apollo-router/src/plugins/cache/invalidation.rs b/apollo-router/src/plugins/cache/invalidation.rs index 45d623299c..8e30a8a830 100644 --- a/apollo-router/src/plugins/cache/invalidation.rs +++ b/apollo-router/src/plugins/cache/invalidation.rs @@ -259,4 +259,12 @@ impl InvalidationRequest { | InvalidationRequest::Entity { subgraph, .. } => subgraph, } } + + pub(super) fn kind(&self) -> &'static str { + match self { + InvalidationRequest::Subgraph { .. } => "subgraph", + InvalidationRequest::Type { .. } => "type", + InvalidationRequest::Entity { .. } => "entity", + } + } } diff --git a/apollo-router/src/plugins/cache/invalidation_endpoint.rs b/apollo-router/src/plugins/cache/invalidation_endpoint.rs index 0b1af7a9b3..19d4b5a582 100644 --- a/apollo-router/src/plugins/cache/invalidation_endpoint.rs +++ b/apollo-router/src/plugins/cache/invalidation_endpoint.rs @@ -12,6 +12,7 @@ use serde::Serialize; use serde_json_bytes::json; use tower::BoxError; use tower::Service; +use tracing::Span; use tracing_futures::Instrument; use super::entity::Subgraph; @@ -19,10 +20,15 @@ use super::invalidation::Invalidation; use super::invalidation::InvalidationOrigin; use crate::configuration::subgraph::SubgraphConfiguration; use crate::plugins::cache::invalidation::InvalidationRequest; +use crate::plugins::telemetry::consts::OTEL_STATUS_CODE; +use crate::plugins::telemetry::consts::OTEL_STATUS_CODE_ERROR; +use crate::plugins::telemetry::consts::OTEL_STATUS_CODE_OK; use crate::services::router; use crate::services::router::body::RouterBody; use crate::ListenAddr; +pub(crate) const INVALIDATION_ENDPOINT_SPAN_NAME: &str = "invalidation_endpoint"; + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)] #[serde(rename_all = "snake_case", deny_unknown_fields, default)] pub(crate) struct SubgraphInvalidationConfig { @@ -102,6 +108,7 @@ impl Service for InvalidationService { async move { let (parts, body) = req.router_request.into_parts(); if !parts.headers.contains_key(AUTHORIZATION) { + Span::current().record(OTEL_STATUS_CODE, OTEL_STATUS_CODE_ERROR); return Ok(router::Response { response: http::Response::builder() .status(StatusCode::UNAUTHORIZED) @@ -114,6 +121,7 @@ impl Service for InvalidationService { Method::POST => { let body = Into::::into(body) .to_bytes() + .instrument(tracing::info_span!("to_bytes")) .await .map_err(|e| format!("failed to get the request body: {e}")) .and_then(|bytes| { @@ -130,14 +138,26 @@ impl Service for InvalidationService { .headers .get(AUTHORIZATION) .ok_or("cannot find authorization header")? - .to_str()?; + .to_str() + .inspect_err(|_err| { + Span::current().record(OTEL_STATUS_CODE, OTEL_STATUS_CODE_ERROR); + })?; match body { Ok(body) => { + Span::current().record( + "invalidation.request.kinds", + body.iter() + .map(|i| i.kind()) + .collect::>() + .join(", "), + ); let valid_shared_key = body.iter().map(|b| b.subgraph_name()).any(|subgraph_name| { valid_shared_key(&config, shared_key, subgraph_name) }); if !valid_shared_key { + Span::current() + .record(OTEL_STATUS_CODE, OTEL_STATUS_CODE_ERROR); return Ok(router::Response { response: http::Response::builder() .status(StatusCode::UNAUTHORIZED) @@ -148,6 +168,7 @@ impl Service for InvalidationService { } match invalidation .invalidate(InvalidationOrigin::Endpoint, body) + .instrument(tracing::info_span!("invalidate")) .await { Ok(count) => Ok(router::Response { @@ -162,34 +183,48 @@ impl Service for InvalidationService { .map_err(BoxError::from)?, context: req.context, }), - Err(err) => Ok(router::Response { - response: http::Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(err.to_string().into()) - .map_err(BoxError::from)?, - context: req.context, - }), + Err(err) => { + Span::current() + .record(OTEL_STATUS_CODE, OTEL_STATUS_CODE_ERROR); + Ok(router::Response { + response: http::Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(err.to_string().into()) + .map_err(BoxError::from)?, + context: req.context, + }) + } } } - Err(err) => Ok(router::Response { - response: http::Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(err.into()) - .map_err(BoxError::from)?, - context: req.context, - }), + Err(err) => { + Span::current().record(OTEL_STATUS_CODE, OTEL_STATUS_CODE_ERROR); + Ok(router::Response { + response: http::Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(err.into()) + .map_err(BoxError::from)?, + context: req.context, + }) + } } } - _ => Ok(router::Response { - response: http::Response::builder() - .status(StatusCode::METHOD_NOT_ALLOWED) - .body("".into()) - .map_err(BoxError::from)?, - context: req.context, - }), + _ => { + Span::current().record(OTEL_STATUS_CODE, OTEL_STATUS_CODE_ERROR); + Ok(router::Response { + response: http::Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .body("".into()) + .map_err(BoxError::from)?, + context: req.context, + }) + } } } - .instrument(tracing::info_span!("invalidation_endpoint")), + .instrument(tracing::info_span!( + INVALIDATION_ENDPOINT_SPAN_NAME, + "invalidation.request.kinds" = ::tracing::field::Empty, + "otel.status_code" = OTEL_STATUS_CODE_OK, + )), ) } } diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-2.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-2.snap index e3d6799c33..7d3bb156a2 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-2.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-2.snap @@ -1,17 +1,7 @@ --- source: apollo-router/src/plugins/cache/tests.rs -expression: response +expression: response.response.headers().get(CACHE_CONTROL) --- -{ - "data": { - "currentUser": { - "activeOrganization": { - "id": "1", - "creatorUser": { - "__typename": "User", - "id": 2 - } - } - } - } -} +Some( + "public", +) diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-3.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-3.snap index 7d3bb156a2..e3d6799c33 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-3.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-3.snap @@ -1,7 +1,17 @@ --- source: apollo-router/src/plugins/cache/tests.rs -expression: response.response.headers().get(CACHE_CONTROL) +expression: response --- -Some( - "public", -) +{ + "data": { + "currentUser": { + "activeOrganization": { + "id": "1", + "creatorUser": { + "__typename": "User", + "id": 2 + } + } + } + } +} diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-4.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-4.snap index e3d6799c33..7d3bb156a2 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-4.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-4.snap @@ -1,17 +1,7 @@ --- source: apollo-router/src/plugins/cache/tests.rs -expression: response +expression: response.response.headers().get(CACHE_CONTROL) --- -{ - "data": { - "currentUser": { - "activeOrganization": { - "id": "1", - "creatorUser": { - "__typename": "User", - "id": 2 - } - } - } - } -} +Some( + "public", +) diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-5.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-5.snap new file mode 100644 index 0000000000..dd1ee738c2 --- /dev/null +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-5.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/cache/tests.rs +expression: cache_keys +--- +[ + { + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:d0b09a1a50750b5e95f73a196acf6ef5a8d60bf19599854b0dbee5dec6ee7ed6:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "status": "cached", + "cache_control": "public" + }, + { + "key": "version:1.0:subgraph:user:type:Query:hash:a3b7f56680be04e3ae646cf8a025aed165e8dd0f6c3dc7c95d745f8cb1348083:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "status": "cached", + "cache_control": "public" + } +] diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-6.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-6.snap new file mode 100644 index 0000000000..e3d6799c33 --- /dev/null +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-6.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/plugins/cache/tests.rs +expression: response +--- +{ + "data": { + "currentUser": { + "activeOrganization": { + "id": "1", + "creatorUser": { + "__typename": "User", + "id": 2 + } + } + } + } +} diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap index 7d3bb156a2..1375808b78 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap @@ -1,7 +1,16 @@ --- source: apollo-router/src/plugins/cache/tests.rs -expression: response.response.headers().get(CACHE_CONTROL) +expression: cache_keys --- -Some( - "public", -) +[ + { + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:d0b09a1a50750b5e95f73a196acf6ef5a8d60bf19599854b0dbee5dec6ee7ed6:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "status": "new", + "cache_control": "public" + }, + { + "key": "version:1.0:subgraph:user:type:Query:hash:a3b7f56680be04e3ae646cf8a025aed165e8dd0f6c3dc7c95d745f8cb1348083:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "status": "new", + "cache_control": "public" + } +] diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__missing_entities-2.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__missing_entities-2.snap index 9798af179e..0944c313df 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__missing_entities-2.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__missing_entities-2.snap @@ -28,7 +28,10 @@ expression: response "currentUser", "allOrganizations", 2 - ] + ], + "extensions": { + "service": "orga" + } } ] } diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-2.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-2.snap index 6e58a2d437..b9832aaeaa 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-2.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-2.snap @@ -10,30 +10,11 @@ expression: response "id": "1", "name": "Organization 1" }, - { - "id": "2", - "name": null - }, { "id": "3", "name": "Organization 3" } ] } - }, - "errors": [ - { - "message": "HTTP fetch failed from 'orga': orga not found", - "path": [ - "currentUser", - "allOrganizations", - 1 - ], - "extensions": { - "code": "SUBREQUEST_HTTP_ERROR", - "service": "orga", - "reason": "orga not found" - } - } - ] + } } diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-3.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-3.snap new file mode 100644 index 0000000000..1322b59275 --- /dev/null +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-3.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/cache/tests.rs +expression: cache_keys +--- +[ + { + "key": "version:1.0:subgraph:orga:type:Organization:entity:5221ff42b311b757445c096c023cee4fefab5de49735e421c494f1119326317b:hash:cffb47a84aff0aea6a447e33caf3b275bdc7f71689d75f56647242b3b9f5e13b:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "status": "cached", + "cache_control": "[REDACTED]" + }, + { + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:cffb47a84aff0aea6a447e33caf3b275bdc7f71689d75f56647242b3b9f5e13b:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "status": "cached", + "cache_control": "[REDACTED]" + } +] diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-4.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-4.snap new file mode 100644 index 0000000000..6e58a2d437 --- /dev/null +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-4.snap @@ -0,0 +1,39 @@ +--- +source: apollo-router/src/plugins/cache/tests.rs +expression: response +--- +{ + "data": { + "currentUser": { + "allOrganizations": [ + { + "id": "1", + "name": "Organization 1" + }, + { + "id": "2", + "name": null + }, + { + "id": "3", + "name": "Organization 3" + } + ] + } + }, + "errors": [ + { + "message": "HTTP fetch failed from 'orga': orga not found", + "path": [ + "currentUser", + "allOrganizations", + 1 + ], + "extensions": { + "code": "SUBREQUEST_HTTP_ERROR", + "service": "orga", + "reason": "orga not found" + } + } + ] +} diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap index b9832aaeaa..87c750131f 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap @@ -1,20 +1,16 @@ --- source: apollo-router/src/plugins/cache/tests.rs -expression: response +expression: cache_keys --- -{ - "data": { - "currentUser": { - "allOrganizations": [ - { - "id": "1", - "name": "Organization 1" - }, - { - "id": "3", - "name": "Organization 3" - } - ] - } +[ + { + "key": "version:1.0:subgraph:orga:type:Organization:entity:5221ff42b311b757445c096c023cee4fefab5de49735e421c494f1119326317b:hash:cffb47a84aff0aea6a447e33caf3b275bdc7f71689d75f56647242b3b9f5e13b:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "status": "new", + "cache_control": "[REDACTED]" + }, + { + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:cffb47a84aff0aea6a447e33caf3b275bdc7f71689d75f56647242b3b9f5e13b:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "status": "new", + "cache_control": "[REDACTED]" } -} +] diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap index 3c363a2b4d..c9839a8823 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap @@ -1,9 +1,16 @@ --- source: apollo-router/src/plugins/cache/tests.rs -expression: response +expression: cache_keys --- -{ - "data": { - "currentUser": null +[ + { + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:d0b09a1a50750b5e95f73a196acf6ef5a8d60bf19599854b0dbee5dec6ee7ed6:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "status": "cached", + "cache_control": "private" + }, + { + "key": "version:1.0:subgraph:user:type:Query:hash:a3b7f56680be04e3ae646cf8a025aed165e8dd0f6c3dc7c95d745f8cb1348083:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "status": "cached", + "cache_control": "private" } -} +] diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-4.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-4.snap new file mode 100644 index 0000000000..e3d6799c33 --- /dev/null +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-4.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/plugins/cache/tests.rs +expression: response +--- +{ + "data": { + "currentUser": { + "activeOrganization": { + "id": "1", + "creatorUser": { + "__typename": "User", + "id": 2 + } + } + } + } +} diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-5.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-5.snap new file mode 100644 index 0000000000..c9839a8823 --- /dev/null +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-5.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/cache/tests.rs +expression: cache_keys +--- +[ + { + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:d0b09a1a50750b5e95f73a196acf6ef5a8d60bf19599854b0dbee5dec6ee7ed6:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "status": "cached", + "cache_control": "private" + }, + { + "key": "version:1.0:subgraph:user:type:Query:hash:a3b7f56680be04e3ae646cf8a025aed165e8dd0f6c3dc7c95d745f8cb1348083:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "status": "cached", + "cache_control": "private" + } +] diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-6.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-6.snap new file mode 100644 index 0000000000..3c363a2b4d --- /dev/null +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-6.snap @@ -0,0 +1,9 @@ +--- +source: apollo-router/src/plugins/cache/tests.rs +expression: response +--- +{ + "data": { + "currentUser": null + } +} diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap index e3d6799c33..69027e4644 100644 --- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap +++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap @@ -1,17 +1,16 @@ --- source: apollo-router/src/plugins/cache/tests.rs -expression: response +expression: cache_keys --- -{ - "data": { - "currentUser": { - "activeOrganization": { - "id": "1", - "creatorUser": { - "__typename": "User", - "id": 2 - } - } - } +[ + { + "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:d0b09a1a50750b5e95f73a196acf6ef5a8d60bf19599854b0dbee5dec6ee7ed6:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "status": "new", + "cache_control": "private" + }, + { + "key": "version:1.0:subgraph:user:type:Query:hash:a3b7f56680be04e3ae646cf8a025aed165e8dd0f6c3dc7c95d745f8cb1348083:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "status": "new", + "cache_control": "private" } -} +] diff --git a/apollo-router/src/plugins/cache/tests.rs b/apollo-router/src/plugins/cache/tests.rs index 64c96a0763..3633a21bcc 100644 --- a/apollo-router/src/plugins/cache/tests.rs +++ b/apollo-router/src/plugins/cache/tests.rs @@ -16,7 +16,10 @@ use super::entity::EntityCache; use crate::cache::redis::RedisCacheStorage; use crate::plugin::test::MockSubgraph; use crate::plugin::test::MockSubgraphService; +use crate::plugins::cache::entity::CacheKeyContext; +use crate::plugins::cache::entity::CacheKeysContext; use crate::plugins::cache::entity::Subgraph; +use crate::plugins::cache::entity::CONTEXT_CACHE_KEYS; use crate::services::subgraph; use crate::services::supergraph; use crate::Context; @@ -160,7 +163,7 @@ async fn insert() { ).with_header(CACHE_CONTROL, HeaderValue::from_static("public")).build()), ("orga", MockSubgraph::builder().with_json( serde_json::json!{{ - "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on Organization{creatorUser{__typename id}}}}", + "query": "query($representations:[_Any!]!){_entities(representations:$representations){..._generated_onOrganization1_0}}fragment _generated_onOrganization1_0 on Organization{creatorUser{__typename id}}", "variables": { "representations": [ { @@ -227,6 +230,10 @@ async fn insert() { .build() .unwrap(); let mut response = service.oneshot(request).await.unwrap(); + let cache_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap(); + let mut cache_keys: Vec = cache_keys.into_values().flatten().collect(); + cache_keys.sort(); + insta::assert_json_snapshot!(cache_keys); insta::assert_debug_snapshot!(response.response.headers().get(CACHE_CONTROL)); let response = response.next_response().await.unwrap(); @@ -255,6 +262,11 @@ async fn insert() { let mut response = service.oneshot(request).await.unwrap(); insta::assert_debug_snapshot!(response.response.headers().get(CACHE_CONTROL)); + let cache_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap(); + let mut cache_keys: Vec = cache_keys.into_values().flatten().collect(); + cache_keys.sort(); + insta::assert_json_snapshot!(cache_keys); + let response = response.next_response().await.unwrap(); insta::assert_json_snapshot!(response); @@ -274,7 +286,7 @@ async fn no_cache_control() { ).build()), ("orga", MockSubgraph::builder().with_json( serde_json::json!{{ - "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on Organization{creatorUser{__typename id}}}}", + "query": "query($representations:[_Any!]!){_entities(representations:$representations){..._generated_onOrganization1_0}}fragment _generated_onOrganization1_0 on Organization{creatorUser{__typename id}}", "variables": { "representations": [ { @@ -365,7 +377,7 @@ async fn private() { .build()), ("orga", MockSubgraph::builder().with_json( serde_json::json!{{ - "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on Organization{creatorUser{__typename id}}}}", + "query": "query($representations:[_Any!]!){_entities(representations:$representations){..._generated_onOrganization1_0}}fragment _generated_onOrganization1_0 on Organization{creatorUser{__typename id}}", "variables": { "representations": [ { @@ -434,14 +446,13 @@ async fn private() { .context(context) .build() .unwrap(); - let response = service - .oneshot(request) - .await - .unwrap() - .next_response() - .await - .unwrap(); + let mut response = service.clone().oneshot(request).await.unwrap(); + let cache_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap(); + let mut cache_keys: Vec = cache_keys.into_values().flatten().collect(); + cache_keys.sort(); + insta::assert_json_snapshot!(cache_keys); + let response = response.next_response().await.unwrap(); insta::assert_json_snapshot!(response); println!("\nNOW WITHOUT SUBGRAPHS\n"); @@ -463,14 +474,13 @@ async fn private() { .context(context) .build() .unwrap(); - let response = service - .clone() - .oneshot(request) - .await - .unwrap() - .next_response() - .await - .unwrap(); + let mut response = service.clone().oneshot(request).await.unwrap(); + let cache_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap(); + let mut cache_keys: Vec = cache_keys.into_values().flatten().collect(); + cache_keys.sort(); + insta::assert_json_snapshot!(cache_keys); + + let response = response.next_response().await.unwrap(); insta::assert_json_snapshot!(response); @@ -483,15 +493,16 @@ async fn private() { .context(context) .build() .unwrap(); - let response = service - .clone() - .oneshot(request) - .await - .unwrap() - .next_response() - .await - .unwrap(); + let mut response = service.clone().oneshot(request).await.unwrap(); + assert!(response + .context + .get::<_, CacheKeysContext>(CONTEXT_CACHE_KEYS) + .ok() + .flatten() + .is_none()); + insta::assert_json_snapshot!(cache_keys); + let response = response.next_response().await.unwrap(); insta::assert_json_snapshot!(response); } @@ -589,6 +600,18 @@ async fn no_data() { .unwrap(); let mut response = service.oneshot(request).await.unwrap(); + let cache_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap(); + let mut cache_keys: Vec = cache_keys.into_values().flatten().collect(); + cache_keys.sort(); + insta::assert_json_snapshot!(cache_keys, { + "[].cache_control" => insta::dynamic_redaction(|value, _path| { + let cache_control = value.as_str().unwrap().to_string(); + assert!(cache_control.contains("max-age=")); + assert!(cache_control.contains("public")); + "[REDACTED]" + }) + }); + let response = response.next_response().await.unwrap(); insta::assert_json_snapshot!(response); @@ -652,6 +675,18 @@ async fn no_data() { .build() .unwrap(); let mut response = service.oneshot(request).await.unwrap(); + + let cache_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap(); + let mut cache_keys: Vec = cache_keys.into_values().flatten().collect(); + cache_keys.sort(); + insta::assert_json_snapshot!(cache_keys, { + "[].cache_control" => insta::dynamic_redaction(|value, _path| { + let cache_control = value.as_str().unwrap().to_string(); + assert!(cache_control.contains("max-age=")); + assert!(cache_control.contains("public")); + "[REDACTED]" + }) + }); let response = response.next_response().await.unwrap(); insta::assert_json_snapshot!(response); diff --git a/apollo-router/src/plugins/coprocessor/mod.rs b/apollo-router/src/plugins/coprocessor/mod.rs index c986f2629a..cbc5b88213 100644 --- a/apollo-router/src/plugins/coprocessor/mod.rs +++ b/apollo-router/src/plugins/coprocessor/mod.rs @@ -78,7 +78,9 @@ impl Plugin for CoprocessorPlugin { type Config = Conf; async fn new(init: PluginInit) -> Result { - let mut http_connector = new_async_http_connector()?; + let client_config = init.config.client.clone().unwrap_or_default(); + let mut http_connector = + new_async_http_connector(client_config.dns_resolution_strategy.unwrap_or_default())?; http_connector.set_nodelay(true); http_connector.set_keepalive(Some(std::time::Duration::from_secs(60))); http_connector.enforce_http(false); @@ -93,9 +95,8 @@ impl Plugin for CoprocessorPlugin { .https_or_http() .enable_http1(); - let connector = if init.config.client.is_none() - || init.config.client.as_ref().unwrap().experimental_http2 != Some(Http2Config::Disable) - { + let experimental_http2 = client_config.experimental_http2.unwrap_or_default(); + let connector = if experimental_http2 != Http2Config::Disable { builder.enable_http2().wrap_connector(http_connector) } else { builder.wrap_connector(http_connector) @@ -106,11 +107,7 @@ impl Plugin for CoprocessorPlugin { .layer(TimeoutLayer::new(init.config.timeout)) .service( hyper::Client::builder() - .http2_only( - init.config.client.is_some() - && init.config.client.as_ref().unwrap().experimental_http2 - == Some(Http2Config::Http2Only), - ) + .http2_only(experimental_http2 == Http2Config::Http2Only) .pool_idle_timeout(POOL_IDLE_TIMEOUT_DURATION) .build(connector), ), @@ -291,6 +288,8 @@ pub(super) struct SubgraphRequestConf { pub(super) method: bool, /// Send the service name pub(super) service_name: bool, + /// Send the subgraph request id + pub(super) subgraph_request_id: bool, } /// What information is passed to a subgraph request/response stage @@ -310,6 +309,8 @@ pub(super) struct SubgraphResponseConf { pub(super) service_name: bool, /// Send the http status pub(super) status_code: bool, + /// Send the subgraph request id + pub(super) subgraph_request_id: bool, } /// Configures the externalization plugin @@ -1012,6 +1013,9 @@ where let uri = request_config.uri.then(|| parts.uri.to_string()); let subgraph_name = service_name.clone(); let service_name = request_config.service_name.then_some(service_name); + let subgraph_request_id = request_config + .subgraph_request_id + .then_some(request.id.clone()); let payload = Externalizable::subgraph_builder() .stage(PipelineStep::SubgraphRequest) @@ -1023,6 +1027,7 @@ where .method(parts.method.to_string()) .and_service_name(service_name) .and_uri(uri) + .and_subgraph_request_id(subgraph_request_id) .build(); tracing::debug!(?payload, "externalized output"); @@ -1081,6 +1086,7 @@ where response: http_response, context: request.context, subgraph_name: Some(subgraph_name), + id: request.id, }; if let Some(context) = co_processor_output.context { @@ -1168,6 +1174,9 @@ where .transpose()?; let context_to_send = response_config.context.then(|| response.context.clone()); let service_name = response_config.service_name.then_some(service_name); + let subgraph_request_id = response_config + .subgraph_request_id + .then_some(response.id.clone()); let payload = Externalizable::subgraph_builder() .stage(PipelineStep::SubgraphResponse) @@ -1177,6 +1186,7 @@ where .and_context(context_to_send) .and_status_code(status_to_send) .and_service_name(service_name) + .and_subgraph_request_id(subgraph_request_id) .build(); tracing::debug!(?payload, "externalized output"); diff --git a/apollo-router/src/plugins/coprocessor/test.rs b/apollo-router/src/plugins/coprocessor/test.rs index 50786f336d..a2b9374fb9 100644 --- a/apollo-router/src/plugins/coprocessor/test.rs +++ b/apollo-router/src/plugins/coprocessor/test.rs @@ -15,6 +15,7 @@ mod tests { use router::body::RouterBody; use serde_json::json; use serde_json_bytes::Value; + use services::subgraph::SubgraphRequestId; use tower::BoxError; use tower::ServiceExt; @@ -279,12 +280,8 @@ mod tests { let subgraph_stage = SubgraphStage { request: SubgraphRequestConf { condition: Default::default(), - headers: false, - context: false, body: true, - uri: false, - method: false, - service_name: false, + ..Default::default() }, response: Default::default(), }; @@ -342,12 +339,9 @@ mod tests { let subgraph_stage = SubgraphStage { request: SubgraphRequestConf { condition: Default::default(), - headers: false, - context: false, body: true, - uri: false, - method: false, - service_name: false, + subgraph_request_id: true, + ..Default::default() }, response: Default::default(), }; @@ -384,16 +378,27 @@ mod tests { req.subgraph_request.into_body().query.unwrap() ); + // this should be the same as the initial request id + assert_eq!(&*req.id, "5678"); + Ok(subgraph::Response::builder() .data(json!({ "test": 1234_u32 })) .errors(Vec::new()) .extensions(crate::json_ext::Object::new()) .context(req.context) + .id(req.id) .build()) }); - let mock_http_client = mock_with_callback(move |_: http::Request| { + let mock_http_client = mock_with_callback(move |req: http::Request| { Box::pin(async { + let deserialized_request: Externalizable = + serde_json::from_slice(&hyper::body::to_bytes(req.into_body()).await.unwrap()) + .unwrap(); + assert_eq!( + deserialized_request.subgraph_request_id.as_deref(), + Some("5678") + ); Ok(http::Response::builder() .body(RouterBody::from( r#"{ @@ -438,7 +443,8 @@ mod tests { } }, "serviceName": "service name shouldn't change", - "uri": "http://thisurihaschanged" + "uri": "http://thisurihaschanged", + "subgraphRequestId": "9abc" }"#, )) .unwrap()) @@ -452,18 +458,15 @@ mod tests { "my_subgraph_service_name".to_string(), ); - let request = subgraph::Request::fake_builder().build(); + let mut request = subgraph::Request::fake_builder().build(); + request.id = SubgraphRequestId("5678".to_string()); + + let response = service.oneshot(request).await.unwrap(); + assert_eq!("5678", &*response.id); assert_eq!( serde_json_bytes::json!({ "test": 1234_u32 }), - service - .oneshot(request) - .await - .unwrap() - .response - .into_body() - .data - .unwrap() + response.response.into_body().data.unwrap() ); } @@ -480,12 +483,8 @@ mod tests { SelectorOrValue::Value("value".to_string().into()), ]) .into(), - headers: false, - context: false, body: true, - uri: false, - method: false, - service_name: false, + ..Default::default() }, response: Default::default(), }; @@ -554,12 +553,8 @@ mod tests { let subgraph_stage = SubgraphStage { request: SubgraphRequestConf { condition: Default::default(), - headers: false, - context: false, body: true, - uri: false, - method: false, - service_name: false, + ..Default::default() }, response: Default::default(), }; @@ -624,12 +619,8 @@ mod tests { let subgraph_stage = SubgraphStage { request: SubgraphRequestConf { condition: Default::default(), - headers: false, - context: false, body: true, - uri: false, - method: false, - service_name: false, + ..Default::default() }, response: Default::default(), }; @@ -689,11 +680,9 @@ mod tests { request: Default::default(), response: SubgraphResponseConf { condition: Default::default(), - headers: false, - context: false, body: true, - service_name: false, - status_code: false, + subgraph_request_id: true, + ..Default::default() }, }; @@ -703,16 +692,23 @@ mod tests { mock_subgraph_service .expect_call() .returning(|req: subgraph::Request| { + assert_eq!(&*req.id, "5678"); Ok(subgraph::Response::builder() .data(json!({ "test": 1234_u32 })) .errors(Vec::new()) .extensions(crate::json_ext::Object::new()) .context(req.context) + .id(req.id) .build()) }); - let mock_http_client = mock_with_callback(move |_: http::Request| { - Box::pin(async { + let mock_http_client = mock_with_callback(move |r: http::Request| { + Box::pin(async move { + let (_, body) = r.into_parts(); + let body: Value = serde_json::from_slice(&body.to_bytes().await.unwrap()).unwrap(); + let subgraph_id = body.get("subgraphRequestId").unwrap(); + assert_eq!(subgraph_id.as_str(), Some("5678")); + Ok(http::Response::builder() .body(RouterBody::from( r#"{ @@ -756,7 +752,8 @@ mod tests { "accepts-multipart": false, "this-is-a-test-context": 42 } - } + }, + "subgraphRequestId": "9abc" }"#, )) .unwrap()) @@ -770,7 +767,8 @@ mod tests { "my_subgraph_service_name".to_string(), ); - let request = subgraph::Request::fake_builder().build(); + let mut request = subgraph::Request::fake_builder().build(); + request.id = SubgraphRequestId("5678".to_string()); let response = service.oneshot(request).await.unwrap(); @@ -779,6 +777,7 @@ mod tests { response.response.headers().get("cookie").unwrap(), "tasty_cookie=strawberry" ); + assert_eq!(&*response.id, "5678"); assert_eq!( response @@ -807,11 +806,8 @@ mod tests { default: None, }) .into(), - headers: false, - context: false, body: true, - service_name: false, - status_code: false, + ..Default::default() }, }; diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/arbitrary_json_schema.graphql b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/arbitrary_json_schema.graphql new file mode 100644 index 0000000000..2b4d7565f3 --- /dev/null +++ b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/arbitrary_json_schema.graphql @@ -0,0 +1,94 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) { + query: Query +} + +directive @join__directive( + graphs: [join__Graph!] + name: String! + args: join__DirectiveArguments +) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean + overrideLabel: String + contextArguments: [join__ContextArgument!] +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar ArbitraryJson @join__type(graph: SUBGRAPHWITHCUSTOMSCALAR) + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + SUBGRAPHWITHCUSTOMSCALAR + @join__graph(name: "subgraphWithCustomScalar", url: "http://localhost:4001") +} + +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 +} + +input MyInput @join__type(graph: SUBGRAPHWITHCUSTOMSCALAR) { + json: ArbitraryJson +} + +type Query @join__type(graph: SUBGRAPHWITHCUSTOMSCALAR) { + fetch(args: MyInput): String +} diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs index 98f3f275ed..397f33e9b6 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs @@ -22,7 +22,6 @@ use super::DemandControlError; use crate::graphql::Response; use crate::graphql::ResponseVisitor; use crate::json_ext::Object; -use crate::json_ext::ValueExt; use crate::plugins::demand_control::cost_calculator::directives::CostDirective; use crate::plugins::demand_control::cost_calculator::directives::ListSizeDirective; use crate::query_planner::fetch::SubgraphOperation; @@ -50,8 +49,6 @@ fn score_argument( schema: &DemandControlledSchema, variables: &Object, ) -> Result { - let cost_directive = - CostDirective::from_argument(schema.directive_name_map(), argument_definition); let ty = schema .types .get(argument_definition.ty.inner_named_type()) @@ -62,6 +59,9 @@ fn score_argument( argument_definition.ty.inner_named_type() )) })?; + let cost_directive = + CostDirective::from_argument(schema.directive_name_map(), argument_definition) + .or(CostDirective::from_type(schema.directive_name_map(), ty)); match (argument, ty) { (_, ExtendedType::Interface(_)) @@ -99,7 +99,7 @@ fn score_argument( // We make a best effort attempt to score the variable, but some of these may not exist in the variables // sent on the supergraph request, such as `$representations`. if let Some(variable) = variables.get(name.as_str()) { - score_argument(&variable.to_ast(), argument_definition, schema, variables) + score_variable(variable, argument_definition, schema) } else { Ok(0.0) } @@ -109,6 +109,62 @@ fn score_argument( } } +fn score_variable( + variable: &Value, + argument_definition: &Node, + schema: &DemandControlledSchema, +) -> Result { + let ty = schema + .types + .get(argument_definition.ty.inner_named_type()) + .ok_or_else(|| { + DemandControlError::QueryParseFailure(format!( + "Argument {} was found in query, but its type ({}) was not found in the schema", + argument_definition.name, + argument_definition.ty.inner_named_type() + )) + })?; + let cost_directive = + CostDirective::from_argument(schema.directive_name_map(), argument_definition) + .or(CostDirective::from_type(schema.directive_name_map(), ty)); + + match (variable, ty) { + (_, ExtendedType::Interface(_)) + | (_, ExtendedType::Object(_)) + | (_, ExtendedType::Union(_)) => Err(DemandControlError::QueryParseFailure( + format!( + "Argument {} has type {}, but objects, interfaces, and unions are disallowed in this position", + argument_definition.name, + argument_definition.ty.inner_named_type() + ) + )), + + (Value::Object(inner_args), ExtendedType::InputObject(inner_arg_defs)) => { + let mut cost = cost_directive.map_or(1.0, |cost| cost.weight()); + for (arg_name, arg_val) in inner_args { + let arg_def = inner_arg_defs.fields.get(arg_name.as_str()).ok_or_else(|| { + DemandControlError::QueryParseFailure(format!( + "Argument {} was found in query, but its type ({}) was not found in the schema", + argument_definition.name, + argument_definition.ty.inner_named_type() + )) + })?; + cost += score_variable(arg_val, arg_def, schema)?; + } + Ok(cost) + } + (Value::Array(inner_args), _) => { + let mut cost = cost_directive.map_or(0.0, |cost| cost.weight()); + for arg_val in inner_args { + cost += score_variable(arg_val, argument_definition, schema)?; + } + Ok(cost) + } + (Value::Null, _) => Ok(0.0), + _ => Ok(cost_directive.map_or(0.0, |cost| cost.weight())) + } +} + impl StaticCostCalculator { pub(crate) fn new( supergraph_schema: Arc, @@ -578,11 +634,13 @@ mod tests { use ahash::HashMapExt; use apollo_federation::query_plan::query_planner::QueryPlanner; use bytes::Bytes; + use router_bridge::planner::PlanOptions; use test_log::test; use tower::Service; use super::*; use crate::introspection::IntrospectionCache; + use crate::plugins::authorization::CacheKeyMetadata; use crate::query_planner::BridgeQueryPlanner; use crate::services::layers::query_analysis::ParsedDocument; use crate::services::QueryPlannerContent; @@ -681,10 +739,16 @@ mod tests { let ctx = Context::new(); ctx.extensions() - .with_lock(|mut lock| lock.insert::(query)); + .with_lock(|mut lock| lock.insert::(query.clone())); let planner_res = planner - .call(QueryPlannerRequest::new(query_str.to_string(), None, ctx)) + .call(QueryPlannerRequest::new( + query_str.to_string(), + None, + query, + CacheKeyMetadata::default(), + PlanOptions::default(), + )) .await .unwrap(); let query_plan = match planner_res.content.unwrap() { @@ -1065,4 +1129,25 @@ mod tests { assert_eq!(planned_cost_rust(schema, query, variables), 127.0); assert_eq!(actual_cost(schema, query, variables, response), 125.0); } + + #[test] + fn arbitrary_json_as_custom_scalar_in_variables() { + let schema = include_str!("./fixtures/arbitrary_json_schema.graphql"); + let query = r#" + query FetchData($myJsonValue: ArbitraryJson) { + fetch(args: { + json: $myJsonValue + }) + } + "#; + let variables = r#" + { + "myJsonValue": { + "field.with.dots": 1 + } + } + "#; + + assert_eq!(estimated_cost(schema, query, variables), 1.0); + } } diff --git a/apollo-router/src/plugins/expose_query_plan.rs b/apollo-router/src/plugins/expose_query_plan.rs index 78d771554c..657969e27c 100644 --- a/apollo-router/src/plugins/expose_query_plan.rs +++ b/apollo-router/src/plugins/expose_query_plan.rs @@ -218,6 +218,10 @@ mod tests { build_mock_supergraph(serde_json::json! {{ "plugins": { "experimental.expose_query_plan": true + }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, } }}) .await, @@ -231,6 +235,10 @@ mod tests { build_mock_supergraph(serde_json::json! {{ "plugins": { "experimental.expose_query_plan": true + }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, } }}) .await, @@ -245,6 +253,10 @@ mod tests { let supergraph = build_mock_supergraph(serde_json::json! {{ "plugins": { "experimental.expose_query_plan": false + }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, } }}) .await; diff --git a/apollo-router/src/plugins/file_uploads/mod.rs b/apollo-router/src/plugins/file_uploads/mod.rs index fb44aa6b25..6932495ebc 100644 --- a/apollo-router/src/plugins/file_uploads/mod.rs +++ b/apollo-router/src/plugins/file_uploads/mod.rs @@ -254,7 +254,9 @@ fn replace_value_at_path<'a>( // Removes value at path. fn remove_value_at_path<'a>(variables: &'a mut json_ext::Object, path: &'a [String]) { - let _ = get_value_at_path(variables, path).take(); + if let Some(v) = get_value_at_path(variables, path) { + *v = serde_json_bytes::Value::Null; + } } fn get_value_at_path<'a>( diff --git a/apollo-router/src/plugins/headers.rs b/apollo-router/src/plugins/headers.rs index 1e52cd444c..b717eef838 100644 --- a/apollo-router/src/plugins/headers.rs +++ b/apollo-router/src/plugins/headers.rs @@ -396,19 +396,20 @@ impl HeadersService { rename, default, }) => { - if !already_propagated.contains(named.as_str()) { + let target_header = rename.as_ref().unwrap_or(named); + if !already_propagated.contains(target_header.as_str()) { let headers = req.subgraph_request.headers_mut(); let values = req.supergraph_request.headers().get_all(named); if values.iter().count() == 0 { if let Some(default) = default { - headers.append(rename.as_ref().unwrap_or(named), default.clone()); + headers.append(target_header, default.clone()); } } else { for value in values { - headers.append(rename.as_ref().unwrap_or(named), value.clone()); + headers.append(target_header, value.clone()); } } - already_propagated.insert(named.as_str()); + already_propagated.insert(target_header.as_str()); } } Operation::Propagate(Propagate::Matching { matching }) => { @@ -454,6 +455,7 @@ mod test { use std::str::FromStr; use std::sync::Arc; + use subgraph::SubgraphRequestId; use tower::BoxError; use super::*; @@ -786,6 +788,49 @@ mod test { Ok(()) } + #[tokio::test] + async fn test_propagate_multiple() -> Result<(), BoxError> { + let mut mock = MockSubgraphService::new(); + mock.expect_call() + .times(1) + .withf(|request| { + request.assert_headers(vec![ + ("aa", "vaa"), + ("ab", "vab"), + ("ac", "vac"), + ("ra", "vda"), + ("rb", "vda"), + ]) + }) + .returning(example_response); + + let mut service = HeadersLayer::new( + Arc::new(vec![ + Operation::Propagate(Propagate::Named { + named: "da".try_into()?, + rename: Some("ra".try_into()?), + default: None, + }), + Operation::Propagate(Propagate::Named { + named: "da".try_into()?, + rename: Some("rb".try_into()?), + default: None, + }), + // This should not take effect as the header is already propagated + Operation::Propagate(Propagate::Named { + named: "db".try_into()?, + rename: Some("ra".try_into()?), + default: None, + }), + ]), + Arc::new(RESERVED_HEADERS.iter().collect()), + ) + .layer(mock); + + service.ready().await?.call(example_request()).await?; + Ok(()) + } + #[tokio::test] async fn test_propagate_exact_default() -> Result<(), BoxError> { let mut mock = MockSubgraphService::new(); @@ -863,6 +908,7 @@ mod test { query_hash: Default::default(), authorization: Default::default(), executable_document: None, + id: SubgraphRequestId(String::new()), }; service.modify_request(&mut request); let headers = request @@ -935,6 +981,7 @@ mod test { query_hash: Default::default(), authorization: Default::default(), executable_document: None, + id: SubgraphRequestId(String::new()), }; service.modify_request(&mut request); let headers = request @@ -956,6 +1003,7 @@ mod test { http::Response::default(), Context::new(), req.subgraph_name.unwrap_or_default(), + SubgraphRequestId(String::new()), )) } @@ -998,6 +1046,7 @@ mod test { query_hash: Default::default(), authorization: Default::default(), executable_document: None, + id: SubgraphRequestId(String::new()), } } diff --git a/apollo-router/src/plugins/include_subgraph_errors.rs b/apollo-router/src/plugins/include_subgraph_errors.rs index 34348f4ef7..5641d0a506 100644 --- a/apollo-router/src/plugins/include_subgraph_errors.rs +++ b/apollo-router/src/plugins/include_subgraph_errors.rs @@ -42,36 +42,47 @@ impl Plugin for IncludeSubgraphErrors { } fn subgraph_service(&self, name: &str, service: subgraph::BoxService) -> subgraph::BoxService { - // Search for subgraph in our configured subgraph map. - // If we can't find it, use the "all" value - if !*self.config.subgraphs.get(name).unwrap_or(&self.config.all) { - let sub_name_response = name.to_string(); - let sub_name_error = name.to_string(); - return service - .map_response(move |mut response: SubgraphResponse| { - if !response.response.body().errors.is_empty() { + // Search for subgraph in our configured subgraph map. If we can't find it, use the "all" value + let include_subgraph_errors = *self.config.subgraphs.get(name).unwrap_or(&self.config.all); + + let sub_name_response = name.to_string(); + let sub_name_error = name.to_string(); + return service + .map_response(move |mut response: SubgraphResponse| { + let errors = &mut response.response.body_mut().errors; + if !errors.is_empty() { + if include_subgraph_errors { + for error in errors.iter_mut() { + error + .extensions + .entry("service") + .or_insert(sub_name_response.clone().into()); + } + } else { tracing::info!("redacted subgraph({sub_name_response}) errors"); - for error in response.response.body_mut().errors.iter_mut() { + for error in errors.iter_mut() { error.message = REDACTED_ERROR_MESSAGE.to_string(); error.extensions = Object::default(); } } - response - }) - // _error to stop clippy complaining about unused assignments... - .map_err(move |mut _error: BoxError| { + } + + response + }) + .map_err(move |error: BoxError| { + if include_subgraph_errors { + error + } else { // Create a redacted error to replace whatever error we have tracing::info!("redacted subgraph({sub_name_error}) error"); - _error = Box::new(crate::error::FetchError::SubrequestHttpError { + Box::new(crate::error::FetchError::SubrequestHttpError { status_code: None, service: "redacted".to_string(), reason: "redacted".to_string(), - }); - _error - }) - .boxed(); - } - service + }) + } + }) + .boxed(); } } @@ -104,7 +115,7 @@ mod test { use crate::Configuration; static UNREDACTED_PRODUCT_RESPONSE: Lazy = Lazy::new(|| { - Bytes::from_static(r#"{"data":{"topProducts":null},"errors":[{"message":"couldn't find mock for query {\"query\":\"query($first: Int) { topProducts(first: $first) { __typename upc } }\",\"variables\":{\"first\":2}}","path":[],"extensions":{"test":"value","code":"FETCH_ERROR"}}]}"#.as_bytes()) + Bytes::from_static(r#"{"data":{"topProducts":null},"errors":[{"message":"couldn't find mock for query {\"query\":\"query($first: Int) { topProducts(first: $first) { __typename upc } }\",\"variables\":{\"first\":2}}","path":[],"extensions":{"test":"value","code":"FETCH_ERROR","service":"products"}}]}"#.as_bytes()) }); static REDACTED_PRODUCT_RESPONSE: Lazy = Lazy::new(|| { @@ -191,13 +202,19 @@ mod test { let product_service = MockSubgraph::new(product_mocks).with_extensions(extensions); + let mut configuration = Configuration::default(); + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + configuration.supergraph.generate_query_fragments = false; + let configuration = Arc::new(configuration); + let schema = include_str!("../../../apollo-router-benchmarks/benches/fixtures/supergraph.graphql"); - let schema = Schema::parse(schema, &Default::default()).unwrap(); + let schema = Schema::parse(schema, &configuration).unwrap(); + let planner = BridgeQueryPlannerPool::new( Vec::new(), schema.into(), - Default::default(), + Arc::clone(&configuration), NonZeroUsize::new(1).unwrap(), ) .await @@ -207,15 +224,9 @@ mod test { let builder = PluggableSupergraphServiceBuilder::new(planner); - let mut plugins = create_plugins( - &Configuration::default(), - &schema, - subgraph_schemas, - None, - None, - ) - .await - .unwrap(); + let mut plugins = create_plugins(&configuration, &schema, subgraph_schemas, None, None) + .await + .unwrap(); plugins.insert("apollo.include_subgraph_errors".to_string(), plugin); @@ -228,10 +239,10 @@ mod test { let supergraph_creator = builder.build().await.expect("should build"); RouterCreator::new( - QueryAnalysisLayer::new(supergraph_creator.schema(), Default::default()).await, - Arc::new(PersistedQueryLayer::new(&Default::default()).await.unwrap()), + QueryAnalysisLayer::new(supergraph_creator.schema(), Arc::clone(&configuration)).await, + Arc::new(PersistedQueryLayer::new(&configuration).await.unwrap()), Arc::new(supergraph_creator), - Arc::new(Configuration::default()), + configuration, ) .await .unwrap() diff --git a/apollo-router/src/plugins/limits/mod.rs b/apollo-router/src/plugins/limits/mod.rs index ea743c6d2b..ec041a1c02 100644 --- a/apollo-router/src/plugins/limits/mod.rs +++ b/apollo-router/src/plugins/limits/mod.rs @@ -4,6 +4,7 @@ mod limited; use std::error::Error; use async_trait::async_trait; +use bytesize::ByteSize; use http::StatusCode; use schemars::JsonSchema; use serde::Deserialize; @@ -101,6 +102,19 @@ pub(crate) struct Config { /// Limit the size of incoming HTTP requests read from the network, /// to protect against running out of memory. Default: 2000000 (2 MB) pub(crate) http_max_request_bytes: usize, + + /// Limit the maximum number of headers of incoming HTTP1 requests. Default is 100. + /// + /// If router receives more headers than the buffer size, it responds to the client with + /// "431 Request Header Fields Too Large". + /// + pub(crate) http1_max_request_headers: Option, + + /// Limit the maximum buffer size for the HTTP1 connection. + /// + /// Default is ~400kib. + #[schemars(with = "Option", default)] + pub(crate) http1_max_request_buf_size: Option, } impl Default for Config { @@ -113,6 +127,8 @@ impl Default for Config { max_aliases: None, warn_only: false, http_max_request_bytes: 2_000_000, + http1_max_request_headers: None, + http1_max_request_buf_size: None, parser_max_tokens: 15_000, // This is `apollo-parser`’s default, which protects against stack overflow diff --git a/apollo-router/src/plugins/record_replay/replay.rs b/apollo-router/src/plugins/record_replay/replay.rs index dc79380f13..814e3e750e 100644 --- a/apollo-router/src/plugins/record_replay/replay.rs +++ b/apollo-router/src/plugins/record_replay/replay.rs @@ -219,6 +219,7 @@ impl Plugin for Replay { http::Response::new(fetch.response.chunks[0].clone()), req.context.clone(), subgraph_name.clone(), + req.id.clone(), ); let runtime_variables = req.subgraph_request.body().variables.clone(); diff --git a/apollo-router/src/plugins/rhai/engine.rs b/apollo-router/src/plugins/rhai/engine.rs index 23c084cb53..fde2fa8e7d 100644 --- a/apollo-router/src/plugins/rhai/engine.rs +++ b/apollo-router/src/plugins/rhai/engine.rs @@ -692,6 +692,13 @@ mod router_plugin { *obj.uri_mut() = uri; Ok(()) } + + #[rhai_fn(get = "subgraph_request_id", pure, return_raw)] + pub(crate) fn get_subgraph_id( + obj: &mut SharedMut, + ) -> Result> { + Ok(obj.with_mut(|request| request.id.to_string())) + } // End of SubgraphRequest specific section #[rhai_fn(get = "headers", pure, return_raw)] @@ -790,6 +797,13 @@ mod router_plugin { Ok(obj.with_mut(|response| response.response.headers().clone())) } + #[rhai_fn(get = "subgraph_request_id", pure, return_raw)] + pub(crate) fn get_subgraph_id_response( + obj: &mut SharedMut, + ) -> Result> { + Ok(obj.with_mut(|response| response.id.to_string())) + } + /*TODO: reenable when https://github.com/apollographql/router/issues/3642 is decided #[rhai_fn(get = "body", pure, return_raw)] pub(crate) fn get_originating_body_router_response( diff --git a/apollo-router/src/plugins/rhai/tests.rs b/apollo-router/src/plugins/rhai/tests.rs index 7b5843df14..dd3d4080f6 100644 --- a/apollo-router/src/plugins/rhai/tests.rs +++ b/apollo-router/src/plugins/rhai/tests.rs @@ -669,7 +669,7 @@ async fn it_can_process_string_subgraph_forbidden() { if let Err(error) = call_rhai_function("process_subgraph_response_string").await { let processed_error = process_error(error); assert_eq!(processed_error.status, StatusCode::INTERNAL_SERVER_ERROR); - assert_eq!(processed_error.message, Some("rhai execution error: 'Runtime error: I have raised an error (line 244, position 5)'".to_string())); + assert_eq!(processed_error.message, Some("rhai execution error: 'Runtime error: I have raised an error (line 251, position 5)'".to_string())); } else { // Test failed panic!("error processed incorrectly"); @@ -697,7 +697,7 @@ async fn it_cannot_process_om_subgraph_missing_message_and_body() { assert_eq!( processed_error.message, Some( - "rhai execution error: 'Runtime error: #{\"status\": 400} (line 255, position 5)'" + "rhai execution error: 'Runtime error: #{\"status\": 400} (line 262, position 5)'" .to_string() ) ); diff --git a/apollo-router/src/plugins/telemetry/config_new/events.rs b/apollo-router/src/plugins/telemetry/config_new/events.rs index a58264bfb7..c5fac133a2 100644 --- a/apollo-router/src/plugins/telemetry/config_new/events.rs +++ b/apollo-router/src/plugins/telemetry/config_new/events.rs @@ -741,9 +741,13 @@ where } // Stub span to make sure the custom attributes are saved in current span extensions // It won't be extracted or sampled at all - let span = info_span!("supergraph_event_send_event"); - let _entered = span.enter(); - inner.send_event(attributes); + if Span::current().is_none() { + let span = info_span!("supergraph_event_send_event"); + let _entered = span.enter(); + inner.send_event(attributes); + } else { + inner.send_event(attributes); + } } fn on_error(&self, error: &BoxError, ctx: &Context) { diff --git a/apollo-router/src/plugins/telemetry/formatters/json.rs b/apollo-router/src/plugins/telemetry/formatters/json.rs index 8f6551ce14..7bb94bfebd 100644 --- a/apollo-router/src/plugins/telemetry/formatters/json.rs +++ b/apollo-router/src/plugins/telemetry/formatters/json.rs @@ -500,7 +500,7 @@ mod test { .or_else(|| ctx.lookup_current()) .expect("current span expected"); let extracted = extract_dd_trace_id(¤t_span); - assert_eq!(extracted, Some("1234".to_string())); + assert_eq!(extracted, Some("1234".to_string()), "should have trace id"); } } @@ -535,7 +535,7 @@ mod test { } #[test] - #[should_panic] + #[should_panic(expected = "should have trace id")] fn test_missing_dd_attribute() { subscriber::with_default( Registry::default() diff --git a/apollo-router/src/plugins/telemetry/otel/layer.rs b/apollo-router/src/plugins/telemetry/otel/layer.rs index 866bf50a35..309114b20f 100644 --- a/apollo-router/src/plugins/telemetry/otel/layer.rs +++ b/apollo-router/src/plugins/telemetry/otel/layer.rs @@ -31,6 +31,7 @@ use tracing_subscriber::Layer; use super::OtelData; use super::PreSampledTracer; +use crate::plugins::cache::invalidation_endpoint::INVALIDATION_ENDPOINT_SPAN_NAME; use crate::plugins::telemetry::config::Sampler; use crate::plugins::telemetry::config::SamplerOption; use crate::plugins::telemetry::consts::FIELD_EXCEPTION_MESSAGE; @@ -733,6 +734,7 @@ where if meta.name() != REQUEST_SPAN_NAME && meta.name() != ROUTER_SPAN_NAME && meta.name() != SUBSCRIPTION_EVENT_SPAN_NAME + && meta.name() != INVALIDATION_ENDPOINT_SPAN_NAME { return false; } diff --git a/apollo-router/src/plugins/traffic_shaping/deduplication.rs b/apollo-router/src/plugins/traffic_shaping/deduplication.rs index 639d0d12b9..5c2165f775 100644 --- a/apollo-router/src/plugins/traffic_shaping/deduplication.rs +++ b/apollo-router/src/plugins/traffic_shaping/deduplication.rs @@ -49,6 +49,7 @@ impl Clone for CloneSubgraphResponse { response: http_ext::Response::from(&self.0.response).inner, context: self.0.context.clone(), subgraph_name: self.0.subgraph_name.clone(), + id: self.0.id.clone(), }) } } @@ -97,6 +98,7 @@ where let mut receiver = waiter.subscribe(); drop(locked_wait_map); + let _guard = request.context.enter_active_request(); match receiver.recv().await { Ok(value) => { return value @@ -105,6 +107,7 @@ where response.0.response, request.context, request.subgraph_name.unwrap_or_default(), + request.id, ) }) .map_err(|e| e.into()) @@ -121,6 +124,7 @@ where let context = request.context.clone(); let authorization_cache_key = request.authorization.clone(); + let id = request.id.clone(); let cache_key = ((&request.subgraph_request).into(), authorization_cache_key); let res = { // when _drop_signal is dropped, either by getting out of the block, returning @@ -162,6 +166,7 @@ where response.0.response, context, response.0.subgraph_name.unwrap_or_default(), + id, ) }); } diff --git a/apollo-router/src/plugins/traffic_shaping/mod.rs b/apollo-router/src/plugins/traffic_shaping/mod.rs index e77322fffa..4335bfe988 100644 --- a/apollo-router/src/plugins/traffic_shaping/mod.rs +++ b/apollo-router/src/plugins/traffic_shaping/mod.rs @@ -35,6 +35,7 @@ use self::rate::RateLimited; pub(crate) use self::retry::RetryPolicy; use self::timeout::Elapsed; use self::timeout::TimeoutLayer; +use crate::configuration::shared::DnsResolutionStrategy; use crate::error::ConfigurationError; use crate::graphql; use crate::layers::ServiceBuilderExt; @@ -72,6 +73,8 @@ struct Shaping { experimental_retry: Option, /// Enable HTTP2 for subgraphs experimental_http2: Option, + /// DNS resolution strategy for subgraphs + dns_resolution_strategy: Option, } #[derive(PartialEq, Default, Debug, Clone, Deserialize, JsonSchema)] @@ -109,6 +112,11 @@ impl Merge for Shaping { .as_ref() .or(fallback.experimental_http2.as_ref()) .cloned(), + dns_resolution_strategy: self + .dns_resolution_strategy + .as_ref() + .or(fallback.dns_resolution_strategy.as_ref()) + .cloned(), }, } } @@ -444,13 +452,19 @@ impl TrafficShaping { } } - pub(crate) fn enable_subgraph_http2(&self, service_name: &str) -> Http2Config { + pub(crate) fn subgraph_client_config( + &self, + service_name: &str, + ) -> crate::configuration::shared::Client { Self::merge_config( self.config.all.as_ref(), self.config.subgraphs.get(service_name), ) - .and_then(|config| config.shaping.experimental_http2) - .unwrap_or(Http2Config::Enable) + .map(|config| crate::configuration::shared::Client { + experimental_http2: config.shaping.experimental_http2, + dns_resolution_strategy: config.shaping.dns_resolution_strategy, + }) + .unwrap_or_default() } } @@ -564,6 +578,9 @@ mod test { r#" traffic_shaping: deduplicate_variables: true + supergraph: + # TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + generate_query_fragments: false "#, ) .unwrap(); @@ -749,16 +766,19 @@ mod test { } #[tokio::test] - async fn test_enable_subgraph_http2() { + async fn test_subgraph_client_config() { let config = serde_yaml::from_str::( r#" all: experimental_http2: disable + dns_resolution_strategy: ipv6_only subgraphs: products: experimental_http2: enable + dns_resolution_strategy: ipv6_then_ipv4 reviews: experimental_http2: disable + dns_resolution_strategy: ipv4_only router: timeout: 65s "#, @@ -769,9 +789,27 @@ mod test { .await .unwrap(); - assert!(shaping_config.enable_subgraph_http2("products") == Http2Config::Enable); - assert!(shaping_config.enable_subgraph_http2("reviews") == Http2Config::Disable); - assert!(shaping_config.enable_subgraph_http2("this_doesnt_exist") == Http2Config::Disable); + assert_eq!( + shaping_config.subgraph_client_config("products"), + crate::configuration::shared::Client { + experimental_http2: Some(Http2Config::Enable), + dns_resolution_strategy: Some(DnsResolutionStrategy::Ipv6ThenIpv4), + }, + ); + assert_eq!( + shaping_config.subgraph_client_config("reviews"), + crate::configuration::shared::Client { + experimental_http2: Some(Http2Config::Disable), + dns_resolution_strategy: Some(DnsResolutionStrategy::Ipv4Only), + }, + ); + assert_eq!( + shaping_config.subgraph_client_config("this_doesnt_exist"), + crate::configuration::shared::Client { + experimental_http2: Some(Http2Config::Disable), + dns_resolution_strategy: Some(DnsResolutionStrategy::Ipv6Only), + }, + ); } #[tokio::test(flavor = "multi_thread")] diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index f10e424bb3..cc058b5555 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -29,6 +29,7 @@ use tower::Service; use super::PlanNode; use super::QueryKey; use crate::apollo_studio_interop::generate_usage_reporting; +use crate::compute_job; use crate::configuration::QueryPlannerMode; use crate::error::PlanErrors; use crate::error::QueryPlannerError; @@ -43,7 +44,6 @@ use crate::metrics::meter_provider; use crate::plugins::authorization::AuthorizationPlugin; use crate::plugins::authorization::CacheKeyMetadata; use crate::plugins::authorization::UnauthorizedPaths; -use crate::plugins::progressive_override::LABELS_TO_OVERRIDE_KEY; use crate::plugins::telemetry::config::ApolloSignatureNormalizationAlgorithm; use crate::plugins::telemetry::config::Conf as TelemetryConfig; use crate::query_planner::convert::convert_root_query_plan_node; @@ -259,7 +259,8 @@ impl PlannerMode { PlannerMode::Rust(rust_planner) => { let doc = doc.clone(); let rust_planner = rust_planner.clone(); - let (plan, mut root_node) = tokio::task::spawn_blocking(move || { + let priority = compute_job::Priority::P8; // High priority + let (plan, mut root_node) = compute_job::execute(priority, move || { let start = Instant::now(); let query_plan_options = QueryPlanOptions { @@ -594,21 +595,14 @@ impl Service for BridgeQueryPlanner { let QueryPlannerRequest { query: original_query, operation_name, - context, + document, + metadata, + plan_options, } = req; - let metadata = context - .extensions() - .with_lock(|lock| lock.get::().cloned().unwrap_or_default()); let this = self.clone(); let fut = async move { - let mut doc = match context - .extensions() - .with_lock(|lock| lock.get::().cloned()) - { - None => return Err(QueryPlannerError::SpecError(SpecError::UnknownFileId)), - Some(d) => d, - }; + let mut doc = document; let api_schema = this.schema.api_schema(); match add_defer_labels(api_schema, &doc.ast) { @@ -635,19 +629,9 @@ impl Service for BridgeQueryPlanner { operation_name.as_deref(), Arc::new(QueryHash(hash)), )?; - context - .extensions() - .with_lock(|mut lock| lock.insert::(doc.clone())); } } - let plan_options = PlanOptions { - override_conditions: context - .get(LABELS_TO_OVERRIDE_KEY) - .unwrap_or_default() - .unwrap_or_default(), - }; - let res = this .get( QueryKey { @@ -664,27 +648,8 @@ impl Service for BridgeQueryPlanner { match res { Ok(query_planner_content) => Ok(QueryPlannerResponse::builder() .content(query_planner_content) - .context(context) .build()), - Err(e) => { - match &e { - QueryPlannerError::PlanningErrors(pe) => { - context.extensions().with_lock(|mut lock| { - lock.insert(Arc::new(pe.usage_reporting.clone())) - }); - } - QueryPlannerError::SpecError(e) => { - context.extensions().with_lock(|mut lock| { - lock.insert(Arc::new(UsageReporting { - stats_report_key: e.get_error_key().to_string(), - referenced_fields_by_type: HashMap::new(), - })) - }); - } - _ => (), - } - Err(e) - } + Err(e) => Err(e), } }; diff --git a/apollo-router/src/query_planner/bridge_query_planner_pool.rs b/apollo-router/src/query_planner/bridge_query_planner_pool.rs index 722a965efd..5246b79901 100644 --- a/apollo-router/src/query_planner/bridge_query_planner_pool.rs +++ b/apollo-router/src/query_planner/bridge_query_planner_pool.rs @@ -4,6 +4,7 @@ use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; use std::sync::Arc; use std::sync::Mutex; +use std::task::Poll; use std::time::Instant; use apollo_compiler::validation::Valid; @@ -21,6 +22,7 @@ use tower::ServiceExt; use super::bridge_query_planner::BridgeQueryPlanner; use super::QueryPlanResult; +use crate::configuration::QueryPlannerMode; use crate::error::QueryPlannerError; use crate::error::ServiceBuildError; use crate::introspection::IntrospectionCache; @@ -36,13 +38,10 @@ static CHANNEL_SIZE: usize = 1_000; #[derive(Clone)] pub(crate) struct BridgeQueryPlannerPool { js_planners: Vec>>, - sender: Sender<( - QueryPlannerRequest, - oneshot::Sender>, - )>, + pool_mode: PoolMode, schema: Arc, subgraph_schemas: Arc>>>, - pool_size_gauge: Arc>>>, + compute_jobs_queue_size_gauge: Arc>>>, v8_heap_used: Arc, v8_heap_used_gauge: Arc>>>, v8_heap_total: Arc, @@ -50,6 +49,20 @@ pub(crate) struct BridgeQueryPlannerPool { introspection_cache: Arc, } +#[derive(Clone)] +enum PoolMode { + Pool { + sender: Sender<( + QueryPlannerRequest, + oneshot::Sender>, + )>, + pool_size_gauge: Arc>>>, + }, + PassThrough { + delegate: BridgeQueryPlanner, + }, +} + impl BridgeQueryPlannerPool { pub(crate) async fn new( old_js_planners: Vec>>, @@ -59,79 +72,100 @@ impl BridgeQueryPlannerPool { ) -> Result { let rust_planner = PlannerMode::maybe_rust(&schema, &configuration)?; - let mut join_set = JoinSet::new(); - - let (sender, receiver) = bounded::<( - QueryPlannerRequest, - oneshot::Sender>, - )>(CHANNEL_SIZE); - let mut old_js_planners_iterator = old_js_planners.into_iter(); // All query planners in the pool now share the same introspection cache. // This allows meaningful gauges, and it makes sense that queries should be cached across all planners. let introspection_cache = Arc::new(IntrospectionCache::new(&configuration)); - for _ in 0..size.into() { - let schema = schema.clone(); - let configuration = configuration.clone(); - let rust_planner = rust_planner.clone(); - let introspection_cache = introspection_cache.clone(); - + let pool_mode; + let js_planners; + let subgraph_schemas; + if let QueryPlannerMode::New = configuration.experimental_query_planner_mode { let old_planner = old_js_planners_iterator.next(); - join_set.spawn(async move { - BridgeQueryPlanner::new( - schema, - configuration, - old_planner, - rust_planner, - introspection_cache, - ) - .await - }); - } - - let mut bridge_query_planners = Vec::new(); - - while let Some(task_result) = join_set.join_next().await { - let bridge_query_planner = - task_result.map_err(|e| ServiceBuildError::ServiceError(Box::new(e)))??; - bridge_query_planners.push(bridge_query_planner); - } - - let subgraph_schemas = bridge_query_planners - .first() - .ok_or_else(|| { - ServiceBuildError::QueryPlannerError(QueryPlannerError::PoolProcessing( - "There should be at least 1 Query Planner service in pool".to_string(), - )) - })? - .subgraph_schemas(); - - let js_planners: Vec<_> = bridge_query_planners - .iter() - .filter_map(|p| p.js_planner()) - .collect(); - - for mut planner in bridge_query_planners.into_iter() { - let receiver = receiver.clone(); - - tokio::spawn(async move { - while let Ok((request, res_sender)) = receiver.recv().await { - let svc = match planner.ready().await { - Ok(svc) => svc, - Err(e) => { - let _ = res_sender.send(Err(e)); + let delegate = BridgeQueryPlanner::new( + schema.clone(), + configuration, + old_planner, + rust_planner, + introspection_cache.clone(), + ) + .await?; + js_planners = delegate.js_planner().into_iter().collect::>(); + subgraph_schemas = delegate.subgraph_schemas(); + pool_mode = PoolMode::PassThrough { delegate } + } else { + let mut join_set = JoinSet::new(); + let (sender, receiver) = bounded::<( + QueryPlannerRequest, + oneshot::Sender>, + )>(CHANNEL_SIZE); + + for _ in 0..size.into() { + let schema = schema.clone(); + let configuration = configuration.clone(); + let rust_planner = rust_planner.clone(); + let introspection_cache = introspection_cache.clone(); + + let old_planner = old_js_planners_iterator.next(); + join_set.spawn(async move { + BridgeQueryPlanner::new( + schema, + configuration, + old_planner, + rust_planner, + introspection_cache, + ) + .await + }); + } - continue; - } - }; + let mut bridge_query_planners = Vec::new(); - let res = svc.call(request).await; + while let Some(task_result) = join_set.join_next().await { + let bridge_query_planner = + task_result.map_err(|e| ServiceBuildError::ServiceError(Box::new(e)))??; + bridge_query_planners.push(bridge_query_planner); + } - let _ = res_sender.send(res); - } - }); + subgraph_schemas = bridge_query_planners + .first() + .ok_or_else(|| { + ServiceBuildError::QueryPlannerError(QueryPlannerError::PoolProcessing( + "There should be at least 1 Query Planner service in pool".to_string(), + )) + })? + .subgraph_schemas(); + + js_planners = bridge_query_planners + .iter() + .filter_map(|p| p.js_planner()) + .collect(); + + for mut planner in bridge_query_planners.into_iter() { + let receiver = receiver.clone(); + + tokio::spawn(async move { + while let Ok((request, res_sender)) = receiver.recv().await { + let svc = match planner.ready().await { + Ok(svc) => svc, + Err(e) => { + let _ = res_sender.send(Err(e)); + + continue; + } + }; + + let res = svc.call(request).await; + + let _ = res_sender.send(res); + } + }); + } + pool_mode = PoolMode::Pool { + sender, + pool_size_gauge: Default::default(), + } } let v8_heap_used: Arc = Default::default(); let v8_heap_total: Arc = Default::default(); @@ -148,10 +182,10 @@ impl BridgeQueryPlannerPool { Ok(Self { js_planners, - sender, + pool_mode, schema, subgraph_schemas, - pool_size_gauge: Default::default(), + compute_jobs_queue_size_gauge: Default::default(), v8_heap_used, v8_heap_used_gauge: Default::default(), v8_heap_total, @@ -160,15 +194,22 @@ impl BridgeQueryPlannerPool { }) } - fn create_pool_size_gauge(&self) -> ObservableGauge { - let sender = self.sender.clone(); - let meter = meter_provider().meter("apollo/router"); - meter - .u64_observable_gauge("apollo.router.query_planning.queued") - .with_description("Number of queries waiting to be planned") - .with_unit(Unit::new("query")) - .with_callback(move |m| m.observe(sender.len() as u64, &[])) - .init() + fn create_pool_size_gauge(&self) { + if let PoolMode::Pool { + sender, + pool_size_gauge, + } = &self.pool_mode + { + let sender = sender.clone(); + let meter = meter_provider().meter("apollo/router"); + let gauge = meter + .u64_observable_gauge("apollo.router.query_planning.queued") + .with_description("Number of queries waiting to be planned") + .with_unit(Unit::new("query")) + .with_callback(move |m| m.observe(sender.len() as u64, &[])) + .init(); + *pool_size_gauge.lock().expect("lock poisoned") = Some(gauge); + } } fn create_heap_used_gauge(&self) -> ObservableGauge { @@ -228,7 +269,11 @@ impl BridgeQueryPlannerPool { pub(super) fn activate(&self) { // Gauges MUST be initialized after a meter provider is created. // When a hot reload happens this means that the gauges must be re-initialized. - *self.pool_size_gauge.lock().expect("lock poisoned") = Some(self.create_pool_size_gauge()); + *self + .compute_jobs_queue_size_gauge + .lock() + .expect("lock poisoned") = Some(crate::compute_job::create_queue_size_gauge()); + self.create_pool_size_gauge(); *self.v8_heap_used_gauge.lock().expect("lock poisoned") = Some(self.create_heap_used_gauge()); *self.v8_heap_total_gauge.lock().expect("lock poisoned") = @@ -244,22 +289,20 @@ impl tower::Service for BridgeQueryPlannerPool { type Future = BoxFuture<'static, Result>; - fn poll_ready( - &mut self, - _cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - if self.sender.is_full() { - std::task::Poll::Ready(Err(QueryPlannerError::PoolProcessing( - "query plan queue is full".into(), - ))) - } else { - std::task::Poll::Ready(Ok(())) + fn poll_ready(&mut self, _cx: &mut std::task::Context<'_>) -> Poll> { + if crate::compute_job::is_full() { + return Poll::Pending; + } + match &self.pool_mode { + PoolMode::Pool { sender, .. } if sender.is_full() => Poll::Ready(Err( + QueryPlannerError::PoolProcessing("query plan queue is full".into()), + )), + _ => Poll::Ready(Ok(())), } } fn call(&mut self, req: QueryPlannerRequest) -> Self::Future { - let (response_sender, response_receiver) = oneshot::channel(); - let sender = self.sender.clone(); + let pool_mode = self.pool_mode.clone(); let get_metrics_future = if let Some(bridge_query_planner) = self.js_planners.first().cloned() { @@ -273,12 +316,22 @@ impl tower::Service for BridgeQueryPlannerPool { }; Box::pin(async move { - let start = Instant::now(); - let _ = sender.send((req, response_sender)).await; - - let res = response_receiver - .await - .map_err(|_| QueryPlannerError::UnhandledPlannerResult)?; + let start; + let res = match pool_mode { + PoolMode::Pool { sender, .. } => { + let (response_sender, response_receiver) = oneshot::channel(); + start = Instant::now(); + let _ = sender.send((req, response_sender)).await; + + response_receiver + .await + .map_err(|_| QueryPlannerError::UnhandledPlannerResult)? + } + PoolMode::PassThrough { mut delegate } => { + start = Instant::now(); + delegate.call(req).await + } + }; f64_histogram!( "apollo.router.query_planning.total.duration", @@ -300,9 +353,11 @@ impl tower::Service for BridgeQueryPlannerPool { mod tests { use opentelemetry_sdk::metrics::data::Gauge; + use router_bridge::planner::PlanOptions; use super::*; use crate::metrics::FutureMetricsExt; + use crate::plugins::authorization::CacheKeyMetadata; use crate::spec::Query; use crate::Context; @@ -326,11 +381,19 @@ mod tests { let doc = Query::parse_document(&query, None, &schema, &config).unwrap(); let context = Context::new(); - context.extensions().with_lock(|mut lock| lock.insert(doc)); - - pool.call(QueryPlannerRequest::new(query, None, context)) - .await - .unwrap(); + context + .extensions() + .with_lock(|mut lock| lock.insert(doc.clone())); + + pool.call(QueryPlannerRequest::new( + query, + None, + doc, + CacheKeyMetadata::default(), + PlanOptions::default(), + )) + .await + .unwrap(); let metrics = crate::metrics::collect_metrics(); let heap_used = metrics.find("apollo.router.v8.heap.used").unwrap(); diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index 6ef9a671e8..209adcc856 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::hash::Hash; -use std::ops::Deref; +use std::hash::Hasher; use std::sync::Arc; use std::task; @@ -14,7 +14,6 @@ use router_bridge::planner::PlanOptions; use router_bridge::planner::Planner; use router_bridge::planner::QueryPlannerConfig; use router_bridge::planner::UsageReporting; -use serde::Serialize; use sha2::Digest; use sha2::Sha256; use tower::BoxError; @@ -36,7 +35,6 @@ use crate::plugins::authorization::CacheKeyMetadata; use crate::plugins::progressive_override::LABELS_TO_OVERRIDE_KEY; use crate::plugins::telemetry::utils::Timer; use crate::query_planner::fetch::SubgraphSchemas; -use crate::query_planner::labeler::add_defer_labels; use crate::query_planner::BridgeQueryPlannerPool; use crate::query_planner::QueryPlanResult; use crate::services::layers::persisted_queries::PersistedQueryLayer; @@ -49,7 +47,6 @@ use crate::services::QueryPlannerResponse; use crate::spec::Schema; use crate::spec::SpecError; use crate::Configuration; -use crate::Context; /// An [`IndexMap`] of available plugins. pub(crate) type Plugins = IndexMap>; @@ -57,11 +54,9 @@ pub(crate) type InMemoryCachePlanner = InMemoryCache>>; pub(crate) const APOLLO_OPERATION_ID: &str = "apollo_operation_id"; -#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Hash)] pub(crate) enum ConfigMode { - //FIXME: add the Rust planner structure once it is hashable and serializable, - // for now use the JS config as it expected to be identical to the Rust one - Rust(Arc), + Rust(Arc), Both(Arc), BothBestEffort(Arc), Js(Arc), @@ -80,7 +75,7 @@ pub(crate) struct CachingQueryPlanner { subgraph_schemas: Arc>>>, plugins: Arc, enable_authorization_directives: bool, - config_mode: ConfigMode, + config_mode_hash: Arc, } fn init_query_plan_from_redis( @@ -125,20 +120,34 @@ where let enable_authorization_directives = AuthorizationPlugin::enable_directives(configuration, &schema).unwrap_or(false); - let config_mode = match configuration.experimental_query_planner_mode { + let mut hasher = StructHasher::new(); + match configuration.experimental_query_planner_mode { crate::configuration::QueryPlannerMode::New => { - ConfigMode::Rust(Arc::new(configuration.js_query_planner_config())) + "PLANNER-NEW".hash(&mut hasher); + ConfigMode::Rust(Arc::new(configuration.rust_query_planner_config())) + .hash(&mut hasher); } crate::configuration::QueryPlannerMode::Legacy => { - ConfigMode::Js(Arc::new(configuration.js_query_planner_config())) + "PLANNER-LEGACY".hash(&mut hasher); + ConfigMode::Js(Arc::new(configuration.js_query_planner_config())).hash(&mut hasher); } crate::configuration::QueryPlannerMode::Both => { + "PLANNER-BOTH".hash(&mut hasher); ConfigMode::Both(Arc::new(configuration.js_query_planner_config())) + .hash(&mut hasher); + ConfigMode::Rust(Arc::new(configuration.rust_query_planner_config())) + .hash(&mut hasher); } crate::configuration::QueryPlannerMode::BothBestEffort => { + "PLANNER-BOTH-BEST-EFFORT".hash(&mut hasher); ConfigMode::BothBestEffort(Arc::new(configuration.js_query_planner_config())) + .hash(&mut hasher); + ConfigMode::Rust(Arc::new(configuration.rust_query_planner_config())) + .hash(&mut hasher); } }; + let config_mode_hash = Arc::new(QueryHash(hasher.finalize())); + Ok(Self { cache, delegate, @@ -146,7 +155,7 @@ where subgraph_schemas, plugins: Arc::new(plugins), enable_authorization_directives, - config_mode, + config_mode_hash, }) } @@ -204,7 +213,7 @@ where hash: Some(hash.clone()), metadata: metadata.clone(), plan_options: plan_options.clone(), - config_mode: self.config_mode.clone(), + config_mode: self.config_mode_hash.clone(), }, ) .take(count) @@ -249,7 +258,7 @@ where hash: None, metadata: CacheKeyMetadata::default(), plan_options: PlanOptions::default(), - config_mode: self.config_mode.clone(), + config_mode: self.config_mode_hash.clone(), }); } } @@ -260,7 +269,7 @@ where let mut count = 0usize; let mut reused = 0usize; for WarmUpCachingQueryKey { - mut query, + query, operation_name, hash, metadata, @@ -268,7 +277,6 @@ where config_mode: _, } in all_cache_keys { - let context = Context::new(); let doc = match query_analysis .parse_document(&query, operation_name.as_deref()) .await @@ -284,7 +292,7 @@ where schema_id: Arc::clone(&self.schema.schema_id), metadata, plan_options, - config_mode: self.config_mode.clone(), + config_mode: self.config_mode_hash.clone(), }; if experimental_reuse_query_plans { @@ -326,25 +334,12 @@ where } }; - let schema = self.schema.api_schema(); - if let Ok(modified_query) = add_defer_labels(schema, &doc.ast) { - query = modified_query.to_string(); - } - - context.extensions().with_lock(|mut lock| { - lock.insert::(doc); - lock.insert(caching_key.metadata) - }); - - let _ = context.insert( - LABELS_TO_OVERRIDE_KEY, - caching_key.plan_options.override_conditions.clone(), - ); - let request = QueryPlannerRequest { query, operation_name, - context: context.clone(), + document: doc, + metadata: caching_key.metadata, + plan_options: caching_key.plan_options, }; let res = match service.ready().await { @@ -415,16 +410,16 @@ where let qp = self.clone(); Box::pin(async move { let context = request.context.clone(); - qp.plan(request).await.inspect(|response| { + qp.plan(request).await.inspect(|_response| { if let Some(usage_reporting) = context .extensions() .with_lock(|lock| lock.get::>().cloned()) { - let _ = response.context.insert( + let _ = context.insert( APOLLO_OPERATION_ID, stats_report_key_hash(usage_reporting.stats_report_key.as_str()), ); - let _ = response.context.insert( + let _ = context.insert( "apollo_operation_signature", usage_reporting.stats_report_key.clone(), ); @@ -490,7 +485,7 @@ where schema_id: Arc::clone(&self.schema.schema_id), metadata, plan_options, - config_mode: self.config_mode.clone(), + config_mode: self.config_mode_hash.clone(), }; let context = request.context.clone(); @@ -502,20 +497,17 @@ where .await; if entry.is_first() { let query_planner::CachingRequest { - mut query, + query, operation_name, context, } = request; - let schema = self.schema.api_schema(); - if let Ok(modified_query) = add_defer_labels(schema, &doc.ast) { - query = modified_query.to_string(); - } - let request = QueryPlannerRequest::builder() .query(query) .and_operation_name(operation_name) - .context(context) + .document(doc) + .metadata(caching_key.metadata) + .plan_options(caching_key.plan_options) .build(); // some clients might timeout and cancel the request before query planning is finished, @@ -524,14 +516,22 @@ where // of restarting the query planner until another timeout tokio::task::spawn( async move { - let res = self.delegate.ready().await?.call(request).await; + let service = match self.delegate.ready().await { + Ok(service) => service, + Err(error) => { + let e = Arc::new(error); + let err = e.clone(); + tokio::spawn(async move { + entry.insert(Err(err)).await; + }); + return Err(CacheResolverError::RetrievalError(e)); + } + }; + + let res = service.call(request).await; match res { - Ok(QueryPlannerResponse { - content, - context, - errors, - }) => { + Ok(QueryPlannerResponse { content, errors }) => { if let Some(content) = content.clone() { let can_cache = match &content { // Already cached in an introspection-specific, small-size, @@ -546,6 +546,10 @@ where tokio::spawn(async move { entry.insert(Ok(content)).await; }); + } else { + tokio::spawn(async move { + entry.send(Ok(content)).await; + }); } } @@ -555,11 +559,7 @@ where lock.insert::>(plan.usage_reporting.clone()) }); } - Ok(QueryPlannerResponse { - content, - context, - errors, - }) + Ok(QueryPlannerResponse { content, errors }) } Err(error) => { let e = Arc::new(error); @@ -567,6 +567,11 @@ where tokio::spawn(async move { entry.insert(Err(err)).await; }); + if let Some(usage_reporting) = e.usage_reporting() { + context.extensions().with_lock(|mut lock| { + lock.insert::>(Arc::new(usage_reporting)); + }); + } Err(CacheResolverError::RetrievalError(e)) } } @@ -593,29 +598,13 @@ where }); } - Ok(QueryPlannerResponse::builder() - .content(content) - .context(context) - .build()) + Ok(QueryPlannerResponse::builder().content(content).build()) } Err(error) => { - match error.deref() { - QueryPlannerError::PlanningErrors(pe) => { - request.context.extensions().with_lock(|mut lock| { - lock.insert::>(Arc::new( - pe.usage_reporting.clone(), - )) - }); - } - QueryPlannerError::SpecError(e) => { - request.context.extensions().with_lock(|mut lock| { - lock.insert::>(Arc::new(UsageReporting { - stats_report_key: e.get_error_key().to_string(), - referenced_fields_by_type: HashMap::new(), - })) - }); - } - _ => {} + if let Some(usage_reporting) = error.usage_reporting() { + context.extensions().with_lock(|mut lock| { + lock.insert::>(Arc::new(usage_reporting)); + }); } Err(CacheResolverError::RetrievalError(error)) @@ -632,7 +621,7 @@ fn stats_report_key_hash(stats_report_key: &str) -> String { hex::encode(result) } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) struct CachingQueryKey { pub(crate) query: String, pub(crate) schema_id: Arc, @@ -640,12 +629,12 @@ pub(crate) struct CachingQueryKey { pub(crate) hash: Arc, pub(crate) metadata: CacheKeyMetadata, pub(crate) plan_options: PlanOptions, - pub(crate) config_mode: ConfigMode, + pub(crate) config_mode: Arc, } // Update this key every time the cache key or the query plan format has to change. // When changed it MUST BE CALLED OUT PROMINENTLY IN THE CHANGELOG. -const CACHE_KEY_VERSION: usize = 0; +const CACHE_KEY_VERSION: usize = 1; const FEDERATION_VERSION: &str = std::env!("FEDERATION_VERSION"); impl std::fmt::Display for CachingQueryKey { @@ -654,34 +643,23 @@ impl std::fmt::Display for CachingQueryKey { hasher.update(self.operation.as_deref().unwrap_or("-")); let operation = hex::encode(hasher.finalize()); - let mut hasher = Sha256::new(); - hasher.update(serde_json::to_vec(&self.metadata).expect("serialization should not fail")); - hasher - .update(serde_json::to_vec(&self.plan_options).expect("serialization should not fail")); - hasher - .update(serde_json::to_vec(&self.config_mode).expect("serialization should not fail")); - hasher.update(&*self.schema_id); + let mut hasher = StructHasher::new(); + "^metadata".hash(&mut hasher); + self.metadata.hash(&mut hasher); + "^plan_options".hash(&mut hasher); + self.plan_options.hash(&mut hasher); + "^config_mode".hash(&mut hasher); + self.config_mode.hash(&mut hasher); let metadata = hex::encode(hasher.finalize()); write!( f, - "plan:{}:{}:{}:{}:{}", + "plan:cache:{}:federation:{}:{}:opname:{}:metadata:{}", CACHE_KEY_VERSION, FEDERATION_VERSION, self.hash, operation, metadata, ) } } -impl Hash for CachingQueryKey { - fn hash(&self, state: &mut H) { - self.schema_id.hash(state); - self.hash.0.hash(state); - self.operation.hash(state); - self.metadata.hash(state); - self.plan_options.hash(state); - self.config_mode.hash(state); - } -} - #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub(crate) struct WarmUpCachingQueryKey { pub(crate) query: String, @@ -689,7 +667,33 @@ pub(crate) struct WarmUpCachingQueryKey { pub(crate) hash: Option>, pub(crate) metadata: CacheKeyMetadata, pub(crate) plan_options: PlanOptions, - pub(crate) config_mode: ConfigMode, + pub(crate) config_mode: Arc, +} + +struct StructHasher { + hasher: Sha256, +} + +impl StructHasher { + fn new() -> Self { + Self { + hasher: Sha256::new(), + } + } + fn finalize(self) -> Vec { + self.hasher.finalize().as_slice().into() + } +} + +impl Hasher for StructHasher { + fn finish(&self) -> u64 { + unreachable!() + } + + fn write(&mut self, bytes: &[u8]) { + self.hasher.update(&[0xFF][..]); + self.hasher.update(bytes); + } } impl ValueType for Result> { @@ -721,6 +725,7 @@ mod tests { use crate::spec::Query; use crate::spec::Schema; use crate::Configuration; + use crate::Context; mock! { #[derive(Debug)] @@ -863,10 +868,7 @@ mod tests { plan: Arc::new(query_plan), }; - Ok(QueryPlannerResponse::builder() - .content(qp_content) - .context(Context::new()) - .build()) + Ok(QueryPlannerResponse::builder().content(qp_content).build()) }); planner }); @@ -900,15 +902,15 @@ mod tests { .with_lock(|mut lock| lock.insert::(doc)); for _ in 0..5 { - assert!(planner + let _ = planner .call(query_planner::CachingRequest::new( "query Me { me { username } }".to_string(), Some("".into()), context.clone(), )) .await - .unwrap() - .context + .unwrap(); + assert!(context .extensions() .with_lock(|lock| lock.contains_key::>())); } @@ -941,10 +943,7 @@ mod tests { ), }; - Ok(QueryPlannerResponse::builder() - .content(qp_content) - .context(Context::new()) - .build()) + Ok(QueryPlannerResponse::builder().content(qp_content).build()) }); planner }); diff --git a/apollo-router/src/query_planner/convert.rs b/apollo-router/src/query_planner/convert.rs index 27592834c6..640ecb624f 100644 --- a/apollo-router/src/query_planner/convert.rs +++ b/apollo-router/src/query_planner/convert.rs @@ -324,6 +324,7 @@ impl From<&'_ next::FetchDataPathElement> for crate::json_ext::PathElement { }) } next::FetchDataPathElement::TypenameEquals(value) => Self::Fragment(value.to_string()), + next::FetchDataPathElement::Parent => Self::Key("..".to_owned(), None), } } } diff --git a/apollo-router/src/query_planner/dual_query_planner.rs b/apollo-router/src/query_planner/dual_query_planner.rs index 05f67d6500..04d393c2cf 100644 --- a/apollo-router/src/query_planner/dual_query_planner.rs +++ b/apollo-router/src/query_planner/dual_query_planner.rs @@ -1,6 +1,8 @@ //! Running two query planner implementations and comparing their results use std::borrow::Borrow; +use std::collections::hash_map::HashMap; +use std::fmt::Write; use std::hash::DefaultHasher; use std::hash::Hash; use std::hash::Hasher; @@ -254,20 +256,26 @@ fn fetch_node_matches(this: &FetchNode, other: &FetchNode) -> Result<(), MatchFa requires, variable_usages, operation, - operation_name: _, // ignored (reordered parallel fetches may have different names) + // ignored: + // reordered parallel fetches may have different names + operation_name: _, operation_kind, id, input_rewrites, output_rewrites, context_rewrites, - schema_aware_hash: _, // ignored - authorization, + // ignored + schema_aware_hash: _, + // ignored: + // when running in comparison mode, the rust plan node does not have + // the attached cache key metadata for authorisation, since the rust plan is + // not going to be the one being executed. + authorization: _, } = this; check_match_eq!(*service_name, other.service_name); check_match_eq!(*operation_kind, other.operation_kind); check_match_eq!(*id, other.id); - check_match_eq!(*authorization, other.authorization); check_match!(same_requires(requires, &other.requires)); check_match!(vec_matches_sorted(variable_usages, &other.variable_usages)); check_match!(same_rewrites(input_rewrites, &other.input_rewrites)); @@ -277,23 +285,26 @@ fn fetch_node_matches(this: &FetchNode, other: &FetchNode) -> Result<(), MatchFa Ok(()) } -fn subscription_primary_matches(this: &SubscriptionNode, other: &SubscriptionNode) -> bool { +fn subscription_primary_matches( + this: &SubscriptionNode, + other: &SubscriptionNode, +) -> Result<(), MatchFailure> { let SubscriptionNode { service_name, variable_usages, operation, - operation_name, + operation_name: _, // ignored (reordered parallel fetches may have different names) operation_kind, input_rewrites, output_rewrites, } = this; - *service_name == other.service_name - && *variable_usages == other.variable_usages - && *operation_name == other.operation_name - && *operation_kind == other.operation_kind - && *input_rewrites == other.input_rewrites - && *output_rewrites == other.output_rewrites - && operation_matches(operation, &other.operation).is_ok() + check_match_eq!(*service_name, other.service_name); + check_match_eq!(*operation_kind, other.operation_kind); + check_match!(vec_matches_sorted(variable_usages, &other.variable_usages)); + check_match!(same_rewrites(input_rewrites, &other.input_rewrites)); + check_match!(same_rewrites(output_rewrites, &other.output_rewrites)); + operation_matches(operation, &other.operation)?; + Ok(()) } fn operation_matches( @@ -388,6 +399,9 @@ fn opt_plan_node_matches( } } +//================================================================================================== +// Vec comparison functions + fn vec_matches(this: &[T], other: &[T], item_matches: impl Fn(&T, &T) -> bool) -> bool { this.len() == other.len() && std::iter::zip(this, other).all(|(this, other)| item_matches(this, other)) @@ -417,6 +431,19 @@ fn vec_matches_sorted(this: &[T], other: &[T]) -> bool { } fn vec_matches_sorted_by( + this: &[T], + other: &[T], + compare: impl Fn(&T, &T) -> std::cmp::Ordering, + item_matches: impl Fn(&T, &T) -> bool, +) -> bool { + let mut this_sorted = this.to_owned(); + let mut other_sorted = other.to_owned(); + this_sorted.sort_by(&compare); + other_sorted.sort_by(&compare); + vec_matches(&this_sorted, &other_sorted, item_matches) +} + +fn vec_matches_result_sorted_by( this: &[T], other: &[T], compare: impl Fn(&T, &T) -> std::cmp::Ordering, @@ -448,37 +475,143 @@ fn vec_matches_as_set(this: &[T], other: &[T], item_matches: impl Fn(&T, &T) && vec_includes_as_set(other, this, &item_matches) } -fn vec_matches_result_as_set( +// Forward/reverse mappings from one Vec items (indices) to another. +type VecMapping = (HashMap, HashMap); + +// performs a set comparison, ignoring order +// and returns a mapping from `this` to `other`. +fn vec_matches_as_set_with_mapping( this: &[T], other: &[T], item_matches: impl Fn(&T, &T) -> bool, -) -> Result<(), MatchFailure> { +) -> VecMapping { // Set-inclusion test in both directions - check_match_eq!(this.len(), other.len()); - for (index, this_node) in this.iter().enumerate() { - if !other + // - record forward/reverse mapping from this items <-> other items for reporting mismatches + let mut forward_map: HashMap = HashMap::new(); + let mut reverse_map: HashMap = HashMap::new(); + for (this_pos, this_node) in this.iter().enumerate() { + if let Some(other_pos) = other .iter() - .any(|other_node| item_matches(this_node, other_node)) + .position(|other_node| item_matches(this_node, other_node)) { - return Err(MatchFailure::new(format!( - "mismatched set: missing item[{}]", - index - ))); + forward_map.insert(this_pos, other_pos); + reverse_map.insert(other_pos, this_pos); } } - for other_node in other.iter() { - if !this + for (other_pos, other_node) in other.iter().enumerate() { + if reverse_map.contains_key(&other_pos) { + continue; + } + if let Some(this_pos) = this .iter() - .any(|this_node| item_matches(this_node, other_node)) + .position(|this_node| item_matches(this_node, other_node)) { + forward_map.insert(this_pos, other_pos); + reverse_map.insert(other_pos, this_pos); + } + } + (forward_map, reverse_map) +} + +// Returns a formatted mismatch message and an optional pair of mismatched positions if the pair +// are the only remaining unmatched items. +fn format_mismatch_as_set( + this_len: usize, + other_len: usize, + forward_map: &HashMap, + reverse_map: &HashMap, +) -> Result<(String, Option<(usize, usize)>), std::fmt::Error> { + let mut ret = String::new(); + let buf = &mut ret; + write!(buf, "- mapping from left to right: [")?; + let mut this_missing_pos = None; + for this_pos in 0..this_len { + if this_pos != 0 { + write!(buf, ", ")?; + } + if let Some(other_pos) = forward_map.get(&this_pos) { + write!(buf, "{}", other_pos)?; + } else { + this_missing_pos = Some(this_pos); + write!(buf, "?")?; + } + } + writeln!(buf, "]")?; + + write!(buf, "- left-over on the right: [")?; + let mut other_missing_count = 0; + let mut other_missing_pos = None; + for other_pos in 0..other_len { + if reverse_map.get(&other_pos).is_none() { + if other_missing_count != 0 { + write!(buf, ", ")?; + } + other_missing_count += 1; + other_missing_pos = Some(other_pos); + write!(buf, "{}", other_pos)?; + } + } + write!(buf, "]")?; + let unmatched_pair = if let (Some(this_missing_pos), Some(other_missing_pos)) = + (this_missing_pos, other_missing_pos) + { + if this_len == 1 + forward_map.len() && other_len == 1 + reverse_map.len() { + // Special case: There are only one missing item on each side. They are supposed to + // match each other. + Some((this_missing_pos, other_missing_pos)) + } else { + None + } + } else { + None + }; + Ok((ret, unmatched_pair)) +} + +fn vec_matches_result_as_set( + this: &[T], + other: &[T], + item_matches: impl Fn(&T, &T) -> Result<(), MatchFailure>, +) -> Result { + // Set-inclusion test in both directions + // - record forward/reverse mapping from this items <-> other items for reporting mismatches + let (forward_map, reverse_map) = + vec_matches_as_set_with_mapping(this, other, |a, b| item_matches(a, b).is_ok()); + if forward_map.len() == this.len() && reverse_map.len() == other.len() { + Ok((forward_map, reverse_map)) + } else { + // report mismatch + let Ok((message, unmatched_pair)) = + format_mismatch_as_set(this.len(), other.len(), &forward_map, &reverse_map) + else { + // Exception: Unable to format mismatch report => fallback to most generic message return Err(MatchFailure::new( - "mismatched set: extra item found".to_string(), + "mismatch at vec_matches_result_as_set (failed to format mismatched sets)" + .to_string(), )); + }; + if let Some(unmatched_pair) = unmatched_pair { + // found a unique pair to report => use that pair's error message + let Err(err) = item_matches(&this[unmatched_pair.0], &other[unmatched_pair.1]) else { + // Exception: Unable to format unique pair mismatch error => fallback to overall report + return Err(MatchFailure::new(format!( + "mismatched sets (failed to format unique pair mismatch error):\n{}", + message + ))); + }; + Err(err.add_description(&format!( + "under a sole unmatched pair ({} -> {}) in a set comparison", + unmatched_pair.0, unmatched_pair.1 + ))) + } else { + Err(MatchFailure::new(format!("mismatched sets:\n{}", message))) } } - Ok(()) } +//================================================================================================== +// PlanNode comparison functions + fn option_to_string(name: Option) -> String { name.map_or_else(|| "".to_string(), |name| name.to_string()) } @@ -490,7 +623,7 @@ fn plan_node_matches(this: &PlanNode, other: &PlanNode) -> Result<(), MatchFailu .map_err(|err| err.add_description("under Sequence node"))?; } (PlanNode::Parallel { nodes: this }, PlanNode::Parallel { nodes: other }) => { - vec_matches_result_as_set(this, other, |a, b| plan_node_matches(a, b).is_ok()) + vec_matches_result_as_set(this, other, plan_node_matches) .map_err(|err| err.add_description("under Parallel node"))?; } (PlanNode::Fetch(this), PlanNode::Fetch(other)) => { @@ -523,7 +656,7 @@ fn plan_node_matches(this: &PlanNode, other: &PlanNode) -> Result<(), MatchFailu rest: other_rest, }, ) => { - check_match!(subscription_primary_matches(primary, other_primary)); + subscription_primary_matches(primary, other_primary)?; opt_plan_node_matches(rest, other_rest) .map_err(|err| err.add_description("under Subscription"))?; } @@ -630,17 +763,22 @@ fn hash_selection_key(selection: &Selection) -> u64 { hash_value(&get_selection_key(selection)) } +// Note: This `Selection` struct is a limited version used for the `requires` field. fn same_selection(x: &Selection, y: &Selection) -> bool { - let x_key = get_selection_key(x); - let y_key = get_selection_key(y); - if x_key != y_key { - return false; - } - let x_selections = x.selection_set(); - let y_selections = y.selection_set(); - match (x_selections, y_selections) { - (Some(x), Some(y)) => same_selection_set_sorted(x, y), - (None, None) => true, + match (x, y) { + (Selection::Field(x), Selection::Field(y)) => { + x.name == y.name + && x.alias == y.alias + && match (&x.selections, &y.selections) { + (Some(x), Some(y)) => same_selection_set_sorted(x, y), + (None, None) => true, + _ => false, + } + } + (Selection::InlineFragment(x), Selection::InlineFragment(y)) => { + x.type_condition == y.type_condition + && same_selection_set_sorted(&x.selections, &y.selections) + } _ => false, } } @@ -694,7 +832,6 @@ fn same_ast_document(x: &ast::Document, y: &ast::Document) -> Result<(), MatchFa _ => others.push(def), } } - fragments.sort_by_key(|frag| frag.name.clone()); (operations, fragments, others) } @@ -708,32 +845,49 @@ fn same_ast_document(x: &ast::Document, y: &ast::Document) -> Result<(), MatchFa "Different number of operation definitions" ); + check_match_eq!(x_frags.len(), y_frags.len()); + let mut fragment_map: HashMap = HashMap::new(); + // Assumption: x_frags and y_frags are topologically sorted. + // Thus, we can build the fragment name mapping in a single pass and compare + // fragment definitions using the mapping at the same time, since earlier fragments + // will never reference later fragments. + x_frags.iter().try_fold((), |_, x_frag| { + let y_frag = y_frags + .iter() + .find(|y_frag| same_ast_fragment_definition(x_frag, y_frag, &fragment_map).is_ok()); + if let Some(y_frag) = y_frag { + if x_frag.name != y_frag.name { + // record it only if they are not identical + fragment_map.insert(x_frag.name.clone(), y_frag.name.clone()); + } + Ok(()) + } else { + Err(MatchFailure::new(format!( + "mismatch: no matching fragment definition for {}", + x_frag.name + ))) + } + })?; + check_match_eq!(x_ops.len(), y_ops.len()); x_ops .iter() .zip(y_ops.iter()) .try_fold((), |_, (x_op, y_op)| { - same_ast_operation_definition(x_op, y_op) + same_ast_operation_definition(x_op, y_op, &fragment_map) .map_err(|err| err.add_description("under operation definition")) })?; - check_match_eq!(x_frags.len(), y_frags.len()); - x_frags - .iter() - .zip(y_frags.iter()) - .try_fold((), |_, (x_frag, y_frag)| { - same_ast_fragment_definition(x_frag, y_frag) - .map_err(|err| err.add_description("under fragment definition")) - })?; Ok(()) } fn same_ast_operation_definition( x: &ast::OperationDefinition, y: &ast::OperationDefinition, + fragment_map: &HashMap, ) -> Result<(), MatchFailure> { // Note: Operation names are ignored, since parallel fetches may have different names. check_match_eq!(x.operation_type, y.operation_type); - vec_matches_sorted_by( + vec_matches_result_sorted_by( &x.variables, &y.variables, |a, b| a.name.cmp(&b.name), @@ -743,7 +897,8 @@ fn same_ast_operation_definition( check_match_eq!(x.directives, y.directives); check_match!(same_ast_selection_set_sorted( &x.selection_set, - &y.selection_set + &y.selection_set, + fragment_map, )); Ok(()) } @@ -777,6 +932,15 @@ fn ast_value_maybe_coerced_to(x: &ast::Value, y: &ast::Value) -> bool { } } + // Special case 3: JS QP may convert string to int for custom scalars, while Rust doesn't. + // - Note: This conversion seems a bit difficult to implement in the `apollo-federation`'s + // `coerce_value` function, since IntValue's constructor is private to the crate. + (ast::Value::Int(ref x), ast::Value::String(ref y)) => { + if x.as_str() == y { + return true; + } + } + // Recurse into list items. (ast::Value::List(ref x), ast::Value::List(ref y)) => { if vec_matches(x, y, |xx, yy| { @@ -818,25 +982,41 @@ fn same_variable_definition( fn same_ast_fragment_definition( x: &ast::FragmentDefinition, y: &ast::FragmentDefinition, + fragment_map: &HashMap, ) -> Result<(), MatchFailure> { - check_match_eq!(x.name, y.name); + // Note: Fragment names at definitions are ignored. check_match_eq!(x.type_condition, y.type_condition); check_match_eq!(x.directives, y.directives); check_match!(same_ast_selection_set_sorted( &x.selection_set, - &y.selection_set + &y.selection_set, + fragment_map, )); Ok(()) } -fn get_ast_selection_key(selection: &ast::Selection) -> SelectionKey { +fn same_ast_argument_value(x: &ast::Value, y: &ast::Value) -> bool { + x == y || ast_value_maybe_coerced_to(x, y) +} + +fn same_ast_argument(x: &ast::Argument, y: &ast::Argument) -> bool { + x.name == y.name && same_ast_argument_value(&x.value, &y.value) +} + +fn get_ast_selection_key( + selection: &ast::Selection, + fragment_map: &HashMap, +) -> SelectionKey { match selection { ast::Selection::Field(field) => SelectionKey::Field { response_name: field.response_name().clone(), directives: field.directives.clone(), }, ast::Selection::FragmentSpread(fragment) => SelectionKey::FragmentSpread { - fragment_name: fragment.fragment_name.clone(), + fragment_name: fragment_map + .get(&fragment.fragment_name) + .unwrap_or(&fragment.fragment_name) + .clone(), directives: fragment.directives.clone(), }, ast::Selection::InlineFragment(fragment) => SelectionKey::InlineFragment { @@ -846,54 +1026,68 @@ fn get_ast_selection_key(selection: &ast::Selection) -> SelectionKey { } } -use std::ops::Not; - -/// Get the sub-selections of a selection. -fn get_ast_selection_set(selection: &ast::Selection) -> Option<&Vec> { - match selection { - ast::Selection::Field(field) => field - .selection_set - .is_empty() - .not() - .then(|| &field.selection_set), - ast::Selection::FragmentSpread(_) => None, - ast::Selection::InlineFragment(fragment) => Some(&fragment.selection_set), - } -} - -fn same_ast_selection(x: &ast::Selection, y: &ast::Selection) -> bool { - let x_key = get_ast_selection_key(x); - let y_key = get_ast_selection_key(y); - if x_key != y_key { - return false; - } - let x_selections = get_ast_selection_set(x); - let y_selections = get_ast_selection_set(y); - match (x_selections, y_selections) { - (Some(x), Some(y)) => same_ast_selection_set_sorted(x, y), - (None, None) => true, +fn same_ast_selection( + x: &ast::Selection, + y: &ast::Selection, + fragment_map: &HashMap, +) -> bool { + match (x, y) { + (ast::Selection::Field(x), ast::Selection::Field(y)) => { + x.name == y.name + && x.alias == y.alias + && vec_matches_sorted_by( + &x.arguments, + &y.arguments, + |a, b| a.name.cmp(&b.name), + |a, b| same_ast_argument(a, b), + ) + && x.directives == y.directives + && same_ast_selection_set_sorted(&x.selection_set, &y.selection_set, fragment_map) + } + (ast::Selection::FragmentSpread(x), ast::Selection::FragmentSpread(y)) => { + let mapped_fragment_name = fragment_map + .get(&x.fragment_name) + .unwrap_or(&x.fragment_name); + *mapped_fragment_name == y.fragment_name && x.directives == y.directives + } + (ast::Selection::InlineFragment(x), ast::Selection::InlineFragment(y)) => { + x.type_condition == y.type_condition + && x.directives == y.directives + && same_ast_selection_set_sorted(&x.selection_set, &y.selection_set, fragment_map) + } _ => false, } } -fn hash_ast_selection_key(selection: &ast::Selection) -> u64 { - hash_value(&get_ast_selection_key(selection)) +fn hash_ast_selection_key(selection: &ast::Selection, fragment_map: &HashMap) -> u64 { + hash_value(&get_ast_selection_key(selection, fragment_map)) } -fn same_ast_selection_set_sorted(x: &[ast::Selection], y: &[ast::Selection]) -> bool { - fn sorted_by_selection_key(s: &[ast::Selection]) -> Vec<&ast::Selection> { +// Selections are sorted and compared after renaming x's fragment spreads according to the +// fragment_map. +fn same_ast_selection_set_sorted( + x: &[ast::Selection], + y: &[ast::Selection], + fragment_map: &HashMap, +) -> bool { + fn sorted_by_selection_key<'a>( + s: &'a [ast::Selection], + fragment_map: &HashMap, + ) -> Vec<&'a ast::Selection> { let mut sorted: Vec<&ast::Selection> = s.iter().collect(); - sorted.sort_by_key(|x| hash_ast_selection_key(x)); + sorted.sort_by_key(|x| hash_ast_selection_key(x, fragment_map)); sorted } if x.len() != y.len() { return false; } - sorted_by_selection_key(x) + let x_sorted = sorted_by_selection_key(x, fragment_map); // Map fragment spreads + let y_sorted = sorted_by_selection_key(y, &Default::default()); // Don't map fragment spreads + x_sorted .into_iter() - .zip(sorted_by_selection_key(y)) - .all(|(x, y)| same_ast_selection(x, y)) + .zip(y_sorted) + .all(|(x, y)| same_ast_selection(x, y, fragment_map)) } #[cfg(test)] @@ -987,6 +1181,75 @@ mod ast_comparison_tests { let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); } + + #[test] + fn test_selection_argument_is_compared() { + let op_x = r#"{ x(arg1: "one") }"#; + let op_y = r#"{ x(arg1: "two") }"#; + let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); + let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); + assert!(super::same_ast_document(&ast_x, &ast_y).is_err()); + } + + #[test] + fn test_selection_argument_order() { + let op_x = r#"{ x(arg1: "one", arg2: "two") }"#; + let op_y = r#"{ x(arg2: "two", arg1: "one") }"#; + let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); + let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); + assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); + } + + #[test] + fn test_string_to_id_coercion_difference() { + // JS QP coerces strings into integer for ID type, while Rust QP doesn't. + // This tests a special case that same_ast_document accepts this difference. + let op_x = r#"{ x(id: 123) }"#; + let op_y = r#"{ x(id: "123") }"#; + let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); + let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); + assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); + } + + #[test] + fn test_fragment_definition_different_names() { + let op_x = r#"{ q { ...f1 ...f2 } } fragment f1 on T { x y } fragment f2 on T { w z }"#; + let op_y = r#"{ q { ...g1 ...g2 } } fragment g1 on T { x y } fragment g2 on T { w z }"#; + let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); + let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); + assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); + } + + #[test] + fn test_fragment_definition_different_names_nested_1() { + // Nested fragments have the same name, only top-level fragments have different names. + let op_x = r#"{ q { ...f2 } } fragment f1 on T { x y } fragment f2 on T { z ...f1 }"#; + let op_y = r#"{ q { ...g2 } } fragment f1 on T { x y } fragment g2 on T { z ...f1 }"#; + let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); + let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); + assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); + } + + #[test] + fn test_fragment_definition_different_names_nested_2() { + // Nested fragments have different names. + let op_x = r#"{ q { ...f2 } } fragment f1 on T { x y } fragment f2 on T { z ...f1 }"#; + let op_y = r#"{ q { ...g2 } } fragment g1 on T { x y } fragment g2 on T { z ...g1 }"#; + let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); + let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); + assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); + } + + #[test] + fn test_fragment_definition_different_names_nested_3() { + // Nested fragments have different names. + // Also, fragment definitions are in different order. + let op_x = r#"{ q { ...f2 ...f3 } } fragment f1 on T { x y } fragment f2 on T { z ...f1 } fragment f3 on T { w } "#; + let op_y = r#"{ q { ...g2 ...g3 } } fragment g1 on T { x y } fragment g2 on T { w } fragment g3 on T { z ...g1 }"#; + let ast_x = ast::Document::parse(op_x, "op_x").unwrap(); + let ast_y = ast::Document::parse(op_y, "op_y").unwrap(); + assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); + } } #[cfg(test)] diff --git a/apollo-router/src/query_planner/execution.rs b/apollo-router/src/query_planner/execution.rs index a4e58f82a0..bd23f549ea 100644 --- a/apollo-router/src/query_planner/execution.rs +++ b/apollo-router/src/query_planner/execution.rs @@ -139,7 +139,7 @@ impl PlanNode { ) .in_current_span() .await; - value.deep_merge(v); + value.type_aware_deep_merge(v, parameters.schema); errors.extend(err.into_iter()); } } @@ -167,7 +167,7 @@ impl PlanNode { .collect(); while let Some((v, err)) = stream.next().in_current_span().await { - value.deep_merge(v); + value.type_aware_deep_merge(v, parameters.schema); errors.extend(err.into_iter()); } } @@ -305,7 +305,7 @@ impl PlanNode { "otel.kind" = "INTERNAL" )) .await; - value.deep_merge(v); + value.type_aware_deep_merge(v, parameters.schema); errors.extend(err.into_iter()); let _ = primary_sender.send((value.clone(), errors.clone())); @@ -353,7 +353,7 @@ impl PlanNode { "otel.kind" = "INTERNAL" )) .await; - value.deep_merge(v); + value.type_aware_deep_merge(v, parameters.schema); errors.extend(err.into_iter()); } else if current_dir.is_empty() { // If the condition is on the root selection set and it's the only one @@ -373,7 +373,7 @@ impl PlanNode { "otel.kind" = "INTERNAL" )) .await; - value.deep_merge(v); + value.type_aware_deep_merge(v, parameters.schema); errors.extend(err.into_iter()); } else if current_dir.is_empty() { // If the condition is on the root selection set and it's the only one @@ -451,7 +451,7 @@ impl DeferredNode { if is_depends_empty { let (primary_value, primary_errors) = primary_receiver.recv().await.unwrap_or_default(); - value.deep_merge(primary_value); + value.type_aware_deep_merge(primary_value, &sc); errors.extend(primary_errors) } else { while let Some((v, _remaining)) = stream.next().await { @@ -460,7 +460,7 @@ impl DeferredNode { // or because it is lagging, but here we only send one message so it // will not happen if let Some(Ok((deferred_value, err))) = v { - value.deep_merge(deferred_value); + value.type_aware_deep_merge(deferred_value, &sc); errors.extend(err.into_iter()) } } @@ -499,7 +499,7 @@ impl DeferredNode { if !is_depends_empty { let (primary_value, primary_errors) = primary_receiver.recv().await.unwrap_or_default(); - v.deep_merge(primary_value); + v.type_aware_deep_merge(primary_value, &sc); errors.extend(primary_errors) } @@ -524,7 +524,7 @@ impl DeferredNode { } else { let (primary_value, primary_errors) = primary_receiver.recv().await.unwrap_or_default(); - value.deep_merge(primary_value); + value.type_aware_deep_merge(primary_value, &sc); errors.extend(primary_errors); if let Err(e) = tx diff --git a/apollo-router/src/query_planner/selection.rs b/apollo-router/src/query_planner/selection.rs index 810c1c1ac5..cf58be1244 100644 --- a/apollo-router/src/query_planner/selection.rs +++ b/apollo-router/src/query_planner/selection.rs @@ -23,17 +23,6 @@ pub(crate) enum Selection { InlineFragment(InlineFragment), } -impl Selection { - pub(crate) fn selection_set(&self) -> Option<&[Selection]> { - match self { - Selection::Field(Field { selections, .. }) => selections.as_deref(), - Selection::InlineFragment(InlineFragment { selections, .. }) => { - Some(selections.as_slice()) - } - } - } -} - /// The field that is used #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -186,7 +175,7 @@ pub(crate) fn execute_selection_set<'a>( e.insert(value); } Entry::Occupied(e) => { - e.into_mut().deep_merge(value); + e.into_mut().type_aware_deep_merge(value, schema); } } } diff --git a/apollo-router/src/query_planner/tests.rs b/apollo-router/src/query_planner/tests.rs index 131dc7ba12..766ce9a715 100644 --- a/apollo-router/src/query_planner/tests.rs +++ b/apollo-router/src/query_planner/tests.rs @@ -821,7 +821,13 @@ async fn alias_renaming() { ].into_iter().collect()); let service = TestHarness::builder() - .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) .unwrap() .schema(schema) .extra_plugin(subgraphs) @@ -1264,7 +1270,13 @@ async fn missing_typename_and_fragments_in_requires2() { ].into_iter().collect()); let service = TestHarness::builder() - .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) .unwrap() .schema(schema) .extra_plugin(subgraphs) @@ -1551,7 +1563,13 @@ async fn typename_propagation() { ); let service = TestHarness::builder() - .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) .unwrap() .schema(TYPENAME_PROPAGATION_SCHEMA) .extra_plugin(subgraphs) @@ -1648,7 +1666,13 @@ async fn typename_propagation2() { ); let service = TestHarness::builder() - .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) .unwrap() .schema(TYPENAME_PROPAGATION_SCHEMA) .extra_plugin(subgraphs) @@ -1746,7 +1770,13 @@ async fn typename_propagation3() { ); let service = TestHarness::builder() - .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) .unwrap() .schema(TYPENAME_PROPAGATION_SCHEMA) .extra_plugin(subgraphs) diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs index b7a894e31e..ca40bd0a86 100644 --- a/apollo-router/src/router_factory.rs +++ b/apollo-router/src/router_factory.rs @@ -417,7 +417,7 @@ pub(crate) async fn create_subgraph_services( name, configuration, &tls_root_store, - shaping.enable_subgraph_http2(name), + shaping.subgraph_client_config(name), )?; let http_service_factory = HttpClientServiceFactory::new(http_service, plugins.clone()); diff --git a/apollo-router/src/services/external.rs b/apollo-router/src/services/external.rs index bf3ff9fb9c..c356ddd39c 100644 --- a/apollo-router/src/services/external.rs +++ b/apollo-router/src/services/external.rs @@ -20,6 +20,7 @@ use strum_macros::Display; use tower::BoxError; use tower::Service; +use super::subgraph::SubgraphRequestId; use crate::plugins::telemetry::otel::OpenTelemetrySpanExt; use crate::plugins::telemetry::reload::prepare_context; use crate::query_planner::QueryPlan; @@ -102,6 +103,8 @@ pub(crate) struct Externalizable { pub(crate) has_next: Option, #[serde(skip_serializing_if = "Option::is_none")] query_plan: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) subgraph_request_id: Option, } #[buildstructor::buildstructor] @@ -145,6 +148,7 @@ where service_name: None, has_next: None, query_plan: None, + subgraph_request_id: None, } } @@ -184,6 +188,7 @@ where service_name: None, has_next, query_plan: None, + subgraph_request_id: None, } } @@ -224,6 +229,7 @@ where service_name: None, has_next, query_plan, + subgraph_request_id: None, } } @@ -242,6 +248,7 @@ where method: Option, service_name: Option, uri: Option, + subgraph_request_id: Option, ) -> Self { assert!(matches!( stage, @@ -263,6 +270,7 @@ where service_name, has_next: None, query_plan: None, + subgraph_request_id, } } diff --git a/apollo-router/src/services/hickory_dns_connector.rs b/apollo-router/src/services/hickory_dns_connector.rs index 70e26ab497..987c6ec52f 100644 --- a/apollo-router/src/services/hickory_dns_connector.rs +++ b/apollo-router/src/services/hickory_dns_connector.rs @@ -6,13 +6,17 @@ use std::pin::Pin; use std::task::Context; use std::task::Poll; +use hickory_resolver::config::LookupIpStrategy; +use hickory_resolver::system_conf::read_system_conf; use hickory_resolver::TokioAsyncResolver; use hyper::client::connect::dns::Name; use hyper::client::HttpConnector; use hyper::service::Service; +use crate::configuration::shared::DnsResolutionStrategy; + /// Wrapper around hickory-resolver's -/// [`TokioAsyncResolver`](https://docs.rs/hickory-resolver/latest/hickory_resolver/type.TokioAsyncResolver.html) +/// [`TokioAsyncResolver`](https://docs.rs/hickory-resolver/0.24.1/hickory_resolver/type.TokioAsyncResolver.html) /// /// The resolver runs a background Task which manages dns requests. When a new resolver is created, /// the background task is also created, it needs to be spawned on top of an executor before using the client, @@ -21,11 +25,14 @@ use hyper::service::Service; pub(crate) struct AsyncHyperResolver(TokioAsyncResolver); impl AsyncHyperResolver { - /// constructs a new resolver from default configuration, uses the corresponding method of - /// [`TokioAsyncResolver`](https://docs.rs/hickory-resolver/latest/hickory_resolver/type.TokioAsyncResolver.html) - pub(crate) fn new_from_system_conf() -> Result { - let resolver = TokioAsyncResolver::tokio_from_system_conf()?; - Ok(Self(resolver)) + /// constructs a new resolver from default configuration, using [read_system_conf](https://docs.rs/hickory-resolver/0.24.1/hickory_resolver/system_conf/fn.read_system_conf.html) + fn new_from_system_conf( + dns_resolution_strategy: DnsResolutionStrategy, + ) -> Result { + let (config, mut options) = read_system_conf()?; + options.ip_strategy = dns_resolution_strategy.into(); + + Ok(Self(TokioAsyncResolver::tokio(config, options))) } } @@ -56,8 +63,22 @@ impl Service for AsyncHyperResolver { } } +impl From for LookupIpStrategy { + fn from(value: DnsResolutionStrategy) -> LookupIpStrategy { + match value { + DnsResolutionStrategy::Ipv4Only => LookupIpStrategy::Ipv4Only, + DnsResolutionStrategy::Ipv6Only => LookupIpStrategy::Ipv6Only, + DnsResolutionStrategy::Ipv4AndIpv6 => LookupIpStrategy::Ipv4AndIpv6, + DnsResolutionStrategy::Ipv6ThenIpv4 => LookupIpStrategy::Ipv6thenIpv4, + DnsResolutionStrategy::Ipv4ThenIpv6 => LookupIpStrategy::Ipv4thenIpv6, + } + } +} + /// A helper function to create an http connector and a dns task with the default configuration -pub(crate) fn new_async_http_connector() -> Result, io::Error> { - let resolver = AsyncHyperResolver::new_from_system_conf()?; +pub(crate) fn new_async_http_connector( + dns_resolution_strategy: DnsResolutionStrategy, +) -> Result, io::Error> { + let resolver = AsyncHyperResolver::new_from_system_conf(dns_resolution_strategy)?; Ok(HttpConnector::new_with_resolver(resolver)) } diff --git a/apollo-router/src/services/http.rs b/apollo-router/src/services/http.rs index 7f5d782498..105bb26065 100644 --- a/apollo-router/src/services/http.rs +++ b/apollo-router/src/services/http.rs @@ -47,7 +47,7 @@ impl HttpClientServiceFactory { pub(crate) fn from_config( service: impl Into, configuration: &crate::Configuration, - http2: crate::plugins::traffic_shaping::Http2Config, + client_config: crate::configuration::shared::Client, ) -> Self { use indexmap::IndexMap; @@ -55,7 +55,7 @@ impl HttpClientServiceFactory { service, configuration, &rustls::RootCertStore::empty(), - http2, + client_config, ) .unwrap(); diff --git a/apollo-router/src/services/http/service.rs b/apollo-router/src/services/http/service.rs index 05e93ca5de..d629412f0c 100644 --- a/apollo-router/src/services/http/service.rs +++ b/apollo-router/src/services/http/service.rs @@ -103,7 +103,7 @@ impl HttpClientService { service: impl Into, configuration: &Configuration, tls_root_store: &RootCertStore, - http2: Http2Config, + client_config: crate::configuration::shared::Client, ) -> Result { let name: String = service.into(); let tls_cert_store = configuration @@ -131,15 +131,16 @@ impl HttpClientService { let tls_client_config = generate_tls_client_config(tls_cert_store, client_cert_config)?; - HttpClientService::new(name, http2, tls_client_config) + HttpClientService::new(name, tls_client_config, client_config) } pub(crate) fn new( service: impl Into, - http2: Http2Config, tls_config: ClientConfig, + client_config: crate::configuration::shared::Client, ) -> Result { - let mut http_connector = new_async_http_connector()?; + let mut http_connector = + new_async_http_connector(client_config.dns_resolution_strategy.unwrap_or_default())?; http_connector.set_nodelay(true); http_connector.set_keepalive(Some(std::time::Duration::from_secs(60))); http_connector.enforce_http(false); @@ -149,6 +150,7 @@ impl HttpClientService { .https_or_http() .enable_http1(); + let http2 = client_config.experimental_http2.unwrap_or_default(); let connector = if http2 != Http2Config::Disable { builder.enable_http2().wrap_connector(http_connector) } else { diff --git a/apollo-router/src/services/http/tests.rs b/apollo-router/src/services/http/tests.rs index 892dc30e1b..68bf996939 100644 --- a/apollo-router/src/services/http/tests.rs +++ b/apollo-router/src/services/http/tests.rs @@ -116,7 +116,7 @@ async fn tls_self_signed() { "test", &config, &rustls::RootCertStore::empty(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ) .unwrap(); @@ -173,7 +173,7 @@ async fn tls_custom_root() { "test", &config, &rustls::RootCertStore::empty(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ) .unwrap(); @@ -283,7 +283,7 @@ async fn tls_client_auth() { "test", &config, &rustls::RootCertStore::empty(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ) .unwrap(); @@ -343,11 +343,13 @@ async fn test_subgraph_h2c() { tokio::task::spawn(emulate_h2c_server(listener)); let subgraph_service = HttpClientService::new( "test", - Http2Config::Http2Only, rustls::ClientConfig::builder() .with_safe_defaults() .with_native_roots() .with_no_client_auth(), + crate::configuration::shared::Client::builder() + .experimental_http2(Http2Config::Http2Only) + .build(), ) .expect("can create a HttpService"); @@ -419,11 +421,13 @@ async fn test_compressed_request_response_body() { tokio::task::spawn(emulate_subgraph_compressed_response(listener)); let subgraph_service = HttpClientService::new( "test", - Http2Config::Http2Only, rustls::ClientConfig::builder() .with_safe_defaults() .with_native_roots() .with_no_client_auth(), + crate::configuration::shared::Client::builder() + .experimental_http2(Http2Config::Http2Only) + .build(), ) .expect("can create a HttpService"); diff --git a/apollo-router/src/services/layers/query_analysis.rs b/apollo-router/src/services/layers/query_analysis.rs index ce008c0a0f..2e2fe77eff 100644 --- a/apollo-router/src/services/layers/query_analysis.rs +++ b/apollo-router/src/services/layers/query_analysis.rs @@ -13,10 +13,10 @@ use http::StatusCode; use lru::LruCache; use router_bridge::planner::UsageReporting; use tokio::sync::Mutex; -use tokio::task; use crate::apollo_studio_interop::generate_extended_references; use crate::apollo_studio_interop::ExtendedReferenceStats; +use crate::compute_job; use crate::context::OPERATION_KIND; use crate::context::OPERATION_NAME; use crate::graphql::Error; @@ -89,7 +89,8 @@ impl QueryAnalysisLayer { // parent let span = tracing::info_span!(QUERY_PARSING_SPAN_NAME, "otel.kind" = "INTERNAL"); - task::spawn_blocking(move || { + let priority = compute_job::Priority::P4; // Medium priority + let job = move || { span.in_scope(|| { Query::parse_document( &query, @@ -98,9 +99,12 @@ impl QueryAnalysisLayer { conf.as_ref(), ) }) - }) - .await - .expect("parse_document task panicked") + }; + // TODO: is this correct? + let job = std::panic::AssertUnwindSafe(job); + compute_job::execute(priority, job) + .await + .expect("Query::parse_document panicked") } pub(crate) async fn supergraph_request( @@ -205,7 +209,7 @@ impl QueryAnalysisLayer { doc.executable.clone(), op_name, self.schema.api_schema(), - &request.supergraph_request.body().variables.clone(), + &request.supergraph_request.body().variables, )) } else { None diff --git a/apollo-router/src/services/query_planner.rs b/apollo-router/src/services/query_planner.rs index 494bb7e2c7..d23a0e2ace 100644 --- a/apollo-router/src/services/query_planner.rs +++ b/apollo-router/src/services/query_planner.rs @@ -4,10 +4,12 @@ use std::sync::Arc; use async_trait::async_trait; use derivative::Derivative; +use router_bridge::planner::PlanOptions; use serde::Deserialize; use serde::Serialize; use static_assertions::assert_impl_all; +use super::layers::query_analysis::ParsedDocument; use crate::error::QueryPlannerError; use crate::graphql; use crate::query_planner::QueryPlan; @@ -20,7 +22,9 @@ assert_impl_all!(Request: Send); pub(crate) struct Request { pub(crate) query: String, pub(crate) operation_name: Option, - pub(crate) context: Context, + pub(crate) document: ParsedDocument, + pub(crate) metadata: crate::plugins::authorization::CacheKeyMetadata, + pub(crate) plan_options: PlanOptions, } #[buildstructor::buildstructor] @@ -29,11 +33,19 @@ impl Request { /// /// Required parameters are required in non-testing code to create a QueryPlannerRequest. #[builder] - pub(crate) fn new(query: String, operation_name: Option, context: Context) -> Request { + pub(crate) fn new( + query: String, + operation_name: Option, + document: ParsedDocument, + metadata: crate::plugins::authorization::CacheKeyMetadata, + plan_options: PlanOptions, + ) -> Request { Self { query, operation_name, - context, + document, + metadata, + plan_options, } } } @@ -72,7 +84,6 @@ pub(crate) struct Response { /// Optional in case of error pub(crate) content: Option, pub(crate) errors: Vec, - pub(crate) context: Context, } /// Query, QueryPlan and Introspection data. @@ -92,14 +103,9 @@ impl Response { #[builder] pub(crate) fn new( content: Option, - context: Context, errors: Vec, ) -> Response { - Self { - content, - context, - errors, - } + Self { content, errors } } } diff --git a/apollo-router/src/services/router/service.rs b/apollo-router/src/services/router/service.rs index 9e379ae3c4..e5792c1a4d 100644 --- a/apollo-router/src/services/router/service.rs +++ b/apollo-router/src/services/router/service.rs @@ -214,6 +214,11 @@ impl Service for RouterService { type Future = BoxFuture<'static, Result>; fn poll_ready(&mut self, _cx: &mut std::task::Context<'_>) -> Poll> { + // This service eventually calls `QueryAnalysisLayer::parse_document()` + // which calls `compute_job::execute()` + if crate::compute_job::is_full() { + return Poll::Pending; + } Poll::Ready(Ok(())) } diff --git a/apollo-router/src/services/router/snapshots/apollo_router__services__router__tests__escaped_quotes_in_string_literal.snap b/apollo-router/src/services/router/snapshots/apollo_router__services__router__tests__escaped_quotes_in_string_literal.snap index 9fcfad90a4..4c8165c12c 100644 --- a/apollo-router/src/services/router/snapshots/apollo_router__services__router__tests__escaped_quotes_in_string_literal.snap +++ b/apollo-router/src/services/router/snapshots/apollo_router__services__router__tests__escaped_quotes_in_string_literal.snap @@ -41,7 +41,7 @@ expression: "(graphql_response, &subgraph_query_log)" ( "reviews", Some( - "query TopProducts__reviews__1($representations:[_Any!]!){_entities(representations:$representations){...on Product{reviewsForAuthor(authorID:\"\\\"1\\\"\"){body}}}}", + "query TopProducts__reviews__1($representations:[_Any!]!){_entities(representations:$representations){..._generated_onProduct1_0}}fragment _generated_onProduct1_0 on Product{reviewsForAuthor(authorID:\"\\\"1\\\"\"){body}}", ), ), ], diff --git a/apollo-router/src/services/subgraph.rs b/apollo-router/src/services/subgraph.rs index 23ebc608e3..987eef24a0 100644 --- a/apollo-router/src/services/subgraph.rs +++ b/apollo-router/src/services/subgraph.rs @@ -1,5 +1,6 @@ #![allow(missing_docs)] // FIXME +use std::fmt::Display; use std::pin::Pin; use std::sync::Arc; @@ -7,6 +8,8 @@ use apollo_compiler::validation::Valid; use http::StatusCode; use http::Version; use multimap::MultiMap; +use serde::Deserialize; +use serde::Serialize; use serde_json_bytes::ByteString; use serde_json_bytes::Map as JsonMap; use serde_json_bytes::Value; @@ -35,6 +38,15 @@ pub type BoxService = tower::util::BoxService; pub type BoxCloneService = tower::util::BoxCloneService; pub type ServiceResult = Result; pub(crate) type BoxGqlStream = Pin + Send + Sync>>; +/// unique id for a subgraph request and the related response +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct SubgraphRequestId(pub String); + +impl Display for SubgraphRequestId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} assert_impl_all!(Request: Send); #[non_exhaustive] @@ -62,6 +74,9 @@ pub struct Request { pub(crate) authorization: Arc, pub(crate) executable_document: Option>>, + + /// unique id for this request + pub(crate) id: SubgraphRequestId, } #[buildstructor::buildstructor] @@ -90,6 +105,7 @@ impl Request { query_hash: Default::default(), authorization: Default::default(), executable_document: None, + id: SubgraphRequestId::new(), } } @@ -154,10 +170,36 @@ impl Clone for Request { query_hash: self.query_hash.clone(), authorization: self.authorization.clone(), executable_document: self.executable_document.clone(), + id: self.id.clone(), } } } +impl SubgraphRequestId { + pub fn new() -> Self { + SubgraphRequestId( + uuid::Uuid::new_v4() + .as_hyphenated() + .encode_lower(&mut uuid::Uuid::encode_buffer()) + .to_string(), + ) + } +} + +impl std::ops::Deref for SubgraphRequestId { + type Target = str; + + fn deref(&self) -> &str { + &self.0 + } +} + +impl Default for SubgraphRequestId { + fn default() -> Self { + Self::new() + } +} + assert_impl_all!(Response: Send); #[derive(Debug)] #[non_exhaustive] @@ -167,6 +209,8 @@ pub struct Response { /// Name of the subgraph, it's an Option to not introduce breaking change pub(crate) subgraph_name: Option, pub context: Context, + /// unique id matching the corresponding field in the request + pub(crate) id: SubgraphRequestId, } #[buildstructor::buildstructor] @@ -179,11 +223,13 @@ impl Response { response: http::Response, context: Context, subgraph_name: String, + id: SubgraphRequestId, ) -> Response { Self { response, context, subgraph_name: Some(subgraph_name), + id, } } @@ -202,6 +248,7 @@ impl Response { context: Context, headers: Option>, subgraph_name: Option, + id: Option, ) -> Response { // Build a response let res = graphql::Response::builder() @@ -220,10 +267,16 @@ impl Response { *response.headers_mut() = headers.unwrap_or_default(); + // Warning: the id argument for this builder is an Option to make that a non breaking change + // but this means that if a subgraph response is created explicitely without an id, it will + // be generated here and not match the id from the subgraph request + let id = id.unwrap_or_default(); + Self { response, context, subgraph_name, + id, } } @@ -244,6 +297,7 @@ impl Response { context: Option, headers: Option>, subgraph_name: Option, + id: Option, ) -> Response { Response::new( label, @@ -255,6 +309,7 @@ impl Response { context.unwrap_or_default(), headers, subgraph_name, + id, ) } @@ -276,6 +331,7 @@ impl Response { context: Option, headers: MultiMap, subgraph_name: Option, + id: Option, ) -> Result { Ok(Response::new( label, @@ -287,6 +343,7 @@ impl Response { context.unwrap_or_default(), Some(header_map(headers)?), subgraph_name, + id, )) } @@ -299,6 +356,7 @@ impl Response { status_code: Option, context: Context, subgraph_name: Option, + id: Option, ) -> Result { Ok(Response::new( Default::default(), @@ -310,6 +368,7 @@ impl Response { context, Default::default(), subgraph_name, + id, )) } } diff --git a/apollo-router/src/services/subgraph_service.rs b/apollo-router/src/services/subgraph_service.rs index 2b99f3bd24..9dbb9fb773 100644 --- a/apollo-router/src/services/subgraph_service.rs +++ b/apollo-router/src/services/subgraph_service.rs @@ -44,6 +44,7 @@ use super::http::HttpClientServiceFactory; use super::http::HttpRequest; use super::layers::content_negotiation::GRAPHQL_JSON_RESPONSE_HEADER_VALUE; use super::router::body::RouterBody; +use super::subgraph::SubgraphRequestId; use super::Plugins; use crate::batching::assemble_batch; use crate::batching::BatchQuery; @@ -494,6 +495,7 @@ async fn call_websocket( subgraph_request, subscription_stream, connection_closed_signal, + id: subgraph_request_id, .. } = request; let subscription_stream_tx = @@ -707,6 +709,7 @@ async fn call_websocket( resp.map(|_| graphql::Response::default()), context, service_name, + subgraph_request_id, )) } @@ -807,7 +810,7 @@ fn http_response_to_graphql_response( pub(crate) async fn process_batch( client_factory: HttpClientServiceFactory, service: String, - mut contexts: Vec, + mut contexts: Vec<(Context, SubgraphRequestId)>, mut request: http::Request, listener_count: usize, ) -> Result, FetchError> { @@ -854,6 +857,7 @@ pub(crate) async fn process_batch( let batch_context = contexts .first() .expect("we have at least one context in the batch") + .0 .clone(); let display_body = batch_context.contains_key(LOGGING_DISPLAY_BODY); let client = client_factory.create(&service); @@ -1014,9 +1018,10 @@ pub(crate) async fn process_batch( .map(|mut http_res| { *http_res.headers_mut() = parts.headers.clone(); // Use the original context for the request to create the response - let context = contexts.pop().expect("we have a context for each response"); + let (context, id) = + contexts.pop().expect("we have a context for each response"); let resp = - SubgraphResponse::new_from_response(http_res, context, subgraph_name); + SubgraphResponse::new_from_response(http_res, context, subgraph_name, id); tracing::debug!("we have a resp: {resp:?}"); resp @@ -1047,6 +1052,8 @@ pub(crate) async fn notify_batch_query( Err(e) => { for tx in senders { // Try to notify all waiters. If we can't notify an individual sender, then log an error + // which, unlike failing to notify on success (see below), contains the the entire error + // response. if let Err(log_error) = tx.send(Err(Box::new(e.clone()))).map_err(|error| { FetchError::SubrequestBatchingError { service: service.clone(), @@ -1076,13 +1083,15 @@ pub(crate) async fn notify_batch_query( // graphql_response, so zip_eq shouldn't panic. // Use the tx to send a graphql_response message to each waiter. for (response, sender) in rs.into_iter().zip_eq(senders) { - if let Err(log_error) = - sender - .send(Ok(response)) - .map_err(|error| FetchError::SubrequestBatchingError { - service: service.to_string(), - reason: format!("tx send failed: {error:?}"), - }) + if let Err(log_error) = sender + .send(Ok(response)) + // If we fail to notify the waiter that our request succeeded, do not log + // out the entire response since this may be substantial and/or contain + // PII data. Simply log that the send failed. + .map_err(|_error| FetchError::SubrequestBatchingError { + service: service.to_string(), + reason: "tx send failed".to_string(), + }) { tracing::error!(service, error=%log_error, "failed to notify sender that batch processing succeeded"); } @@ -1094,7 +1103,12 @@ pub(crate) async fn notify_batch_query( } type BatchInfo = ( - (String, http::Request, Vec, usize), + ( + String, + http::Request, + Vec<(Context, SubgraphRequestId)>, + usize, + ), Vec>>, ); @@ -1222,7 +1236,9 @@ pub(crate) async fn call_single_http( }); let SubgraphRequest { - subgraph_request, .. + subgraph_request, + id: subgraph_request_id, + .. } = request; let operation_name = subgraph_request @@ -1353,6 +1369,7 @@ pub(crate) async fn call_single_http( .expect("it won't fail everything is coming from an existing response"), context.clone(), service_name.to_owned(), + subgraph_request_id.clone(), ); should_log = condition.lock().evaluate_response(&subgraph_response); } @@ -1397,6 +1414,7 @@ pub(crate) async fn call_single_http( resp, context, service_name.to_owned(), + subgraph_request_id, )) } @@ -1695,7 +1713,6 @@ mod tests { use crate::plugins::subscription::SubgraphPassthroughMode; use crate::plugins::subscription::SubscriptionModeConfig; use crate::plugins::subscription::SUBSCRIPTION_CALLBACK_HMAC_KEY; - use crate::plugins::traffic_shaping::Http2Config; use crate::protocols::websocket::ClientMessage; use crate::protocols::websocket::ServerMessage; use crate::protocols::websocket::WebSocketProtocol; @@ -2372,7 +2389,7 @@ mod tests { HttpClientServiceFactory::from_config( "testbis", &Configuration::default(), - Http2Config::Disable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -2416,7 +2433,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -2450,7 +2467,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -2485,7 +2502,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -2523,7 +2540,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -2562,7 +2579,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -2605,7 +2622,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -2646,7 +2663,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -2699,7 +2716,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -2743,7 +2760,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -2785,7 +2802,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -2823,7 +2840,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -2861,7 +2878,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -2898,7 +2915,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -2935,7 +2952,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -2981,7 +2998,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -3025,7 +3042,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -3066,7 +3083,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -3107,7 +3124,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); @@ -3148,7 +3165,7 @@ mod tests { HttpClientServiceFactory::from_config( "test", &Configuration::default(), - Http2Config::Enable, + crate::configuration::shared::Client::default(), ), ) .expect("can create a SubgraphService"); diff --git a/apollo-router/src/services/supergraph/service.rs b/apollo-router/src/services/supergraph/service.rs index a217df0bd1..f87c81727f 100644 --- a/apollo-router/src/services/supergraph/service.rs +++ b/apollo-router/src/services/supergraph/service.rs @@ -171,11 +171,7 @@ async fn service_call( let body = req.supergraph_request.body(); let variables = body.variables.clone(); - let QueryPlannerResponse { - content, - context, - errors, - } = match plan_query( + let QueryPlannerResponse { content, errors } = match plan_query( planning, body.operation_name.clone(), context.clone(), diff --git a/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_from_primary_on_deferred_responses-2.snap b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_from_primary_on_deferred_responses-2.snap index 36f064b496..40f678bb4f 100644 --- a/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_from_primary_on_deferred_responses-2.snap +++ b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_from_primary_on_deferred_responses-2.snap @@ -24,7 +24,10 @@ expression: stream.next_response().await.unwrap() "path": [ "computer", "errorField" - ] + ], + "extensions": { + "service": "computers" + } } ] } diff --git a/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_on_deferred_responses-2.snap b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_on_deferred_responses-2.snap index d487599dc5..6cfe12e7a7 100644 --- a/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_on_deferred_responses-2.snap +++ b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_on_deferred_responses-2.snap @@ -17,7 +17,10 @@ expression: stream.next_response().await.unwrap() "message": "error user 0", "path": [ "currentUser" - ] + ], + "extensions": { + "service": "user" + } } ] } diff --git a/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_on_incremental_responses-2.snap b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_on_incremental_responses-2.snap index c36f465d70..8cac0e7164 100644 --- a/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_on_incremental_responses-2.snap +++ b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_on_incremental_responses-2.snap @@ -23,7 +23,10 @@ expression: stream.next_response().await.unwrap() "activeOrganization", "suborga", 0 - ] + ], + "extensions": { + "service": "orga" + } } ] }, @@ -56,7 +59,10 @@ expression: stream.next_response().await.unwrap() "activeOrganization", "suborga", 2 - ] + ], + "extensions": { + "service": "orga" + } } ] } diff --git a/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_on_nullified_paths.snap b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_on_nullified_paths.snap index bf618438ad..4299132063 100644 --- a/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_on_nullified_paths.snap +++ b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__errors_on_nullified_paths.snap @@ -20,7 +20,8 @@ expression: stream.next_response().await.unwrap() "bar" ], "extensions": { - "code": "NOT_FOUND" + "code": "NOT_FOUND", + "service": "S2" } } ] diff --git a/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__missing_entities.snap b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__missing_entities.snap index a4366f1d9a..a046e3aa13 100644 --- a/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__missing_entities.snap +++ b/apollo-router/src/services/supergraph/snapshots/apollo_router__services__supergraph__tests__missing_entities.snap @@ -18,7 +18,10 @@ expression: stream.next_response().await.unwrap() "path": [ "currentUser", "activeOrganization" - ] + ], + "extensions": { + "service": "orga" + } } ] } diff --git a/apollo-router/src/services/supergraph/tests.rs b/apollo-router/src/services/supergraph/tests.rs index b62b83737b..ac2dbab21a 100644 --- a/apollo-router/src/services/supergraph/tests.rs +++ b/apollo-router/src/services/supergraph/tests.rs @@ -655,7 +655,13 @@ async fn deferred_fragment_bounds_nullability() { ].into_iter().collect()); let service = TestHarness::builder() - .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) .unwrap() .schema(SCHEMA) .extra_plugin(subgraphs) @@ -737,7 +743,13 @@ async fn errors_on_incremental_responses() { ].into_iter().collect()); let service = TestHarness::builder() - .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) .unwrap() .schema(SCHEMA) .extra_plugin(subgraphs) @@ -809,7 +821,13 @@ async fn root_typename_with_defer() { ].into_iter().collect()); let service = TestHarness::builder() - .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) .unwrap() .schema(SCHEMA) .extra_plugin(subgraphs) @@ -867,7 +885,18 @@ async fn subscription_with_callback() { ).build()) ].into_iter().collect()); - let mut configuration: Configuration = serde_json::from_value(serde_json::json!({"include_subgraph_errors": { "all": true }, "subscription": { "enabled": true, "mode": {"callback": {"public_url": "http://localhost:4545/callback"}}}})).unwrap(); + let mut configuration: Configuration = serde_json::from_value(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "subscription": { + "enabled": true, + "mode": {"callback": {"public_url": "http://localhost:4545/callback"}} + }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) + .unwrap(); configuration.notify = notify.clone(); let service = TestHarness::builder() .configuration(Arc::new(configuration)) @@ -942,7 +971,23 @@ async fn subscription_callback_schema_reload() { ("orga", orga_subgraph) ].into_iter().collect()); - let mut configuration: Configuration = serde_json::from_value(serde_json::json!({"include_subgraph_errors": { "all": true }, "headers": {"all": {"request": [{"propagate": {"named": "x-test"}}]}}, "subscription": { "enabled": true, "mode": {"callback": {"public_url": "http://localhost:4545/callback"}}}})).unwrap(); + let mut configuration: Configuration = serde_json::from_value(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "headers": { + "all": { + "request": [{"propagate": {"named": "x-test"}}] + } + }, + "subscription": { + "enabled": true, + "mode": {"callback": {"public_url": "http://localhost:4545/callback"}} + }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) + .unwrap(); configuration.notify = notify.clone(); let configuration = Arc::new(configuration); let service = TestHarness::builder() @@ -1011,7 +1056,19 @@ async fn subscription_with_callback_with_limit() { ).build()) ].into_iter().collect()); - let mut configuration: Configuration = serde_json::from_value(serde_json::json!({"include_subgraph_errors": { "all": true }, "subscription": { "enabled": true, "max_opened_subscriptions": 1, "mode": {"callback": {"public_url": "http://localhost:4545/callback"}}}})).unwrap(); + let mut configuration: Configuration = serde_json::from_value(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "subscription": { + "enabled": true, + "max_opened_subscriptions": 1, + "mode": {"callback": {"public_url": "http://localhost:4545/callback"}} + }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) + .unwrap(); configuration.notify = notify.clone(); let mut service = TestHarness::builder() .configuration(Arc::new(configuration)) @@ -1103,12 +1160,29 @@ async fn subscription_without_header() { async fn root_typename_with_defer_and_empty_first_response() { let subgraphs = MockedSubgraphs([ ("user", MockSubgraph::builder().with_json( - serde_json::json!{{"query":"{... on Query{currentUser{activeOrganization{__typename id}}}}"}}, - serde_json::json!{{"data": {"currentUser": { "activeOrganization": { "__typename": "Organization", "id": "0" } }}}} - ).build()), + serde_json::json!{{ + "query": " + { ..._generated_onQuery1_0 } + + fragment _generated_onQuery1_0 on Query { + currentUser { activeOrganization { __typename id} } + } + ", + }}, + serde_json::json!{{"data": {"currentUser": { "activeOrganization": { "__typename": "Organization", "id": "0" } }}}} + ).build()), ("orga", MockSubgraph::builder().with_json( serde_json::json!{{ - "query":"query($representations:[_Any!]!){_entities(representations:$representations){...on Organization{suborga{id name}}}}", + "query": " + query($representations: [_Any!]!) { + _entities(representations: $representations) { + ..._generated_onOrganization1_0 + } + } + fragment _generated_onOrganization1_0 on Organization { + suborga { id name } + } + ", "variables": { "representations":[{"__typename": "Organization", "id":"0"}] } @@ -1216,7 +1290,14 @@ async fn root_typename_with_defer_in_defer() { ).build()), ("orga", MockSubgraph::builder().with_json( serde_json::json!{{ - "query":"query($representations:[_Any!]!){_entities(representations:$representations){...on Organization{suborga{__typename id name}}}}", + "query":" + query($representations:[_Any!]!){ + _entities(representations:$representations) { ..._generated_onOrganization1_0 } + } + fragment _generated_onOrganization1_0 on Organization { + suborga {__typename id name} + } + ", "variables": { "representations":[{"__typename": "Organization", "id":"0"}] } @@ -1453,7 +1534,13 @@ async fn filter_nullified_deferred_responses() { ].into_iter().collect()); let service = TestHarness::builder() - .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) .unwrap() .schema(SCHEMA) .extra_plugin(subgraphs) @@ -1606,7 +1693,13 @@ async fn reconstruct_deferred_query_under_interface() { ].into_iter().collect()); let service = TestHarness::builder() - .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) .unwrap() .schema(schema) .extra_plugin(subgraphs) @@ -1775,7 +1868,13 @@ async fn interface_object_typename_rewrites() { ].into_iter().collect()); let service = TestHarness::builder() - .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) .unwrap() .schema(schema) .extra_plugin(subgraphs) @@ -1913,7 +2012,13 @@ async fn interface_object_response_processing() { ].into_iter().collect()); let service = TestHarness::builder() - .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) .unwrap() .schema(schema) .extra_plugin(subgraphs) @@ -2182,7 +2287,13 @@ async fn aliased_subgraph_data_rewrites_on_root_fetch() { ].into_iter().collect()); let service = TestHarness::builder() - .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) .unwrap() .schema(schema) .extra_plugin(subgraphs) @@ -2322,7 +2433,13 @@ async fn aliased_subgraph_data_rewrites_on_non_root_fetch() { ].into_iter().collect()); let service = TestHarness::builder() - .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + })) .unwrap() .schema(schema) .extra_plugin(subgraphs) @@ -3295,7 +3412,13 @@ async fn fragment_reuse() { ].into_iter().collect()); let service = TestHarness::builder() - .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } })) + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { "all": true }, + "supergraph": { + "generate_query_fragments": false, + "experimental_reuse_query_fragments": true, + } + })) .unwrap() .schema(SCHEMA) .extra_plugin(subgraphs) diff --git a/apollo-router/src/snapshots/apollo_router__batching__tests__it_matches_subgraph_request_ids_to_responses.snap b/apollo-router/src/snapshots/apollo_router__batching__tests__it_matches_subgraph_request_ids_to_responses.snap new file mode 100644 index 0000000000..086b0189a6 --- /dev/null +++ b/apollo-router/src/snapshots/apollo_router__batching__tests__it_matches_subgraph_request_ids_to_responses.snap @@ -0,0 +1,27 @@ +--- +source: apollo-router/src/batching.rs +expression: response +--- +[ + { + "data": { + "entryA": { + "index": 0 + } + } + }, + { + "data": { + "entryA": { + "index": 1 + } + } + }, + { + "data": { + "entryA": { + "index": 2 + } + } + } +] diff --git a/apollo-router/src/test_harness.rs b/apollo-router/src/test_harness.rs index a0b5384489..516048e3d7 100644 --- a/apollo-router/src/test_harness.rs +++ b/apollo-router/src/test_harness.rs @@ -283,6 +283,7 @@ impl<'a> TestHarness<'a> { let empty_response = subgraph::Response::builder() .extensions(crate::json_ext::Object::new()) .context(request.context) + .id(request.id) .build(); std::future::ready(Ok(empty_response)) }) diff --git a/apollo-router/src/testdata/supergraph_with_context.graphql b/apollo-router/src/testdata/supergraph_with_context.graphql index a1627ab52e..77eabbb31c 100644 --- a/apollo-router/src/testdata/supergraph_with_context.graphql +++ b/apollo-router/src/testdata/supergraph_with_context.graphql @@ -22,7 +22,7 @@ directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION -directive @context(name: String!) repeatable on INTERFACE | OBJECT +directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION directive @context__fromContext(field: String) on ARGUMENT_DEFINITION diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index 826a377e04..671c462519 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -587,7 +587,6 @@ impl IntegrationTest { let mut request = builder.json(&query).build().unwrap(); telemetry.inject_context(&mut request); - request.headers_mut().remove(ACCEPT); match client.execute(request).await { Ok(response) => (span_id, response), Err(err) => { diff --git a/apollo-router/tests/fixtures/batching/subgraph_id.rhai b/apollo-router/tests/fixtures/batching/subgraph_id.rhai new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apollo-router/tests/fixtures/batching/subgraph_id.router.yaml b/apollo-router/tests/fixtures/batching/subgraph_id.router.yaml new file mode 100644 index 0000000000..af5deda700 --- /dev/null +++ b/apollo-router/tests/fixtures/batching/subgraph_id.router.yaml @@ -0,0 +1,15 @@ +# Simple config to enable batching and rhai scripts for testing + +batching: + enabled: true + mode: batch_http_link + subgraph: + all: + enabled: true + +rhai: + scripts: ./tests/fixtures/batching + main: subgraph_id.rhai + +include_subgraph_errors: + all: true diff --git a/apollo-router/tests/fixtures/request_response_test.rhai b/apollo-router/tests/fixtures/request_response_test.rhai index 7704b0ea9a..966a1a42b0 100644 --- a/apollo-router/tests/fixtures/request_response_test.rhai +++ b/apollo-router/tests/fixtures/request_response_test.rhai @@ -99,6 +99,10 @@ fn process_subgraph_request(request) { process_common_request(true, true, request); // subgraph doesn't have a context member process_common_request(false, true, request.subgraph); + + if request.subgraph_request_id == () { + throw(`subgraph request must have a subgraph request id`); + } } fn test_response_is_primary(response) { @@ -206,6 +210,9 @@ fn process_subgraph_response(response) { process_common_response(response); test_response_body(response); test_response_status_code(response); + if response.subgraph_request_id == () { + throw(`subgraph response must have a subgraph request id`); + } } fn process_subgraph_response_om_forbidden(response) { diff --git a/apollo-router/tests/fixtures/set_context/one_fetch_failure.json b/apollo-router/tests/fixtures/set_context/one_fetch_failure.json index 5515102f2d..311ebfc23c 100644 --- a/apollo-router/tests/fixtures/set_context/one_fetch_failure.json +++ b/apollo-router/tests/fixtures/set_context/one_fetch_failure.json @@ -41,6 +41,29 @@ } } } + }, + { + "request": { + "query": "query Query_fetch_failure__Subgraph1__2($representations:[_Any!]!$contextualArgument__Subgraph1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument__Subgraph1_0)}}}", + "operationName": "Query_fetch_failure__Subgraph1__2", + "variables": { + "contextualArgument__Subgraph1_0": "prop value", + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": "prop value", + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } } ] } \ No newline at end of file diff --git a/apollo-router/tests/fixtures/set_context/one_null_param.json b/apollo-router/tests/fixtures/set_context/one_null_param.json index f36994a1a0..f4db338c19 100644 --- a/apollo-router/tests/fixtures/set_context/one_null_param.json +++ b/apollo-router/tests/fixtures/set_context/one_null_param.json @@ -38,6 +38,26 @@ ] } } + }, + { + "request": { + "query": "query Query_Null_Param__Subgraph1__1($representations:[_Any!]!$contextualArgument__Subgraph1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument__Subgraph1_0)}}}", + "operationName": "Query_Null_Param__Subgraph1__1", + "variables": { + "contextualArgument__Subgraph1_0": null, + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "1", + "field": 1234 + } + ] + } + } } ] } \ No newline at end of file diff --git a/apollo-router/tests/fixtures/type_conditions/artwork.json b/apollo-router/tests/fixtures/type_conditions/artwork.json index ad5451f398..e4c437c2cd 100644 --- a/apollo-router/tests/fixtures/type_conditions/artwork.json +++ b/apollo-router/tests/fixtures/type_conditions/artwork.json @@ -2,7 +2,7 @@ "mocks": [ { "request": { - "query":"query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){...on EntityCollectionSection{title artwork(params:$movieResultParam)}...on GallerySection{artwork(params:$movieResultParam)}}}", + "query":"query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){..._generated_onEntityCollectionSection2_0 ...on GallerySection{artwork(params:$movieResultParam)}}}fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection{title artwork(params:$movieResultParam)}", "operationName":"Search__artworkSubgraph__1", "variables":{ "movieResultParam":"movieResultEnabled", @@ -50,7 +50,7 @@ }, { "request": { - "query": "query Search__artworkSubgraph__2($representations:[_Any!]!$articleResultParam:String){_entities(representations:$representations){...on GallerySection{artwork(params:$articleResultParam)}...on EntityCollectionSection{artwork(params:$articleResultParam)title}}}", + "query": "query Search__artworkSubgraph__2($representations:[_Any!]!$articleResultParam:String){_entities(representations:$representations){...on GallerySection{artwork(params:$articleResultParam)}..._generated_onEntityCollectionSection2_0}}fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection{artwork(params:$articleResultParam)title}", "operationName": "Search__artworkSubgraph__2", "variables":{ "articleResultParam":"articleResultEnabled", diff --git a/apollo-router/tests/fixtures/type_conditions/artwork_disabled.json b/apollo-router/tests/fixtures/type_conditions/artwork_disabled.json index 1552804424..5e230e1e3c 100644 --- a/apollo-router/tests/fixtures/type_conditions/artwork_disabled.json +++ b/apollo-router/tests/fixtures/type_conditions/artwork_disabled.json @@ -2,7 +2,7 @@ "mocks": [ { "request": { - "query":"query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){...on EntityCollectionSection{title artwork(params:$movieResultParam)}...on GallerySection{artwork(params:$movieResultParam)}}}", + "query":"query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){..._generated_onEntityCollectionSection2_0...on GallerySection{artwork(params:$movieResultParam)}}}fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection{title artwork(params:$movieResultParam)}", "operationName":"Search__artworkSubgraph__1", "variables":{ "representations":[ @@ -79,4 +79,4 @@ } } ] -} \ No newline at end of file +} diff --git a/apollo-router/tests/fixtures/type_conditions/search.json b/apollo-router/tests/fixtures/type_conditions/search.json index 1393fcfd02..85bc7facaa 100644 --- a/apollo-router/tests/fixtures/type_conditions/search.json +++ b/apollo-router/tests/fixtures/type_conditions/search.json @@ -2,7 +2,7 @@ "mocks": [ { "request": { - "query":"query Search__searchSubgraph__0{search{__typename ...on MovieResult{sections{__typename ...on EntityCollectionSection{__typename id}...on GallerySection{__typename id}}id}...on ArticleResult{id sections{__typename ...on GallerySection{__typename id}...on EntityCollectionSection{__typename id}}}}}", + "query":"query Search__searchSubgraph__0{search{__typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0}}fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection{__typename id}fragment _generated_onGallerySection2_0 on GallerySection{__typename id}fragment _generated_onMovieResult2_0 on MovieResult{sections{__typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0}id}fragment _generated_onArticleResult2_0 on ArticleResult{id sections{__typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0}}", "operationName":"Search__searchSubgraph__0" }, "response": { diff --git a/apollo-router/tests/fixtures/type_conditions/search_list_of_list.json b/apollo-router/tests/fixtures/type_conditions/search_list_of_list.json index 228e4ab0db..6197584e2b 100644 --- a/apollo-router/tests/fixtures/type_conditions/search_list_of_list.json +++ b/apollo-router/tests/fixtures/type_conditions/search_list_of_list.json @@ -2,7 +2,7 @@ "mocks": [ { "request": { - "query":"query Search__searchSubgraph__0{searchListOfList{__typename ...on MovieResult{sections{__typename ...on EntityCollectionSection{__typename id}...on GallerySection{__typename id}}id}...on ArticleResult{id sections{__typename ...on GallerySection{__typename id}...on EntityCollectionSection{__typename id}}}}}", + "query": "query Search__searchSubgraph__0 { searchListOfList { __typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { __typename id } fragment _generated_onGallerySection2_0 on GallerySection { __typename id } fragment _generated_onMovieResult2_0 on MovieResult { sections { __typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0 } id } fragment _generated_onArticleResult2_0 on ArticleResult { id sections { __typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0 } }", "operationName":"Search__searchSubgraph__0" }, "response": { @@ -133,4 +133,4 @@ } } ] -} \ No newline at end of file +} diff --git a/apollo-router/tests/fixtures/type_conditions/search_list_of_list_of_list.json b/apollo-router/tests/fixtures/type_conditions/search_list_of_list_of_list.json index 1c2023c5ec..e183b3e65c 100644 --- a/apollo-router/tests/fixtures/type_conditions/search_list_of_list_of_list.json +++ b/apollo-router/tests/fixtures/type_conditions/search_list_of_list_of_list.json @@ -2,7 +2,7 @@ "mocks": [ { "request": { - "query":"query Search__searchSubgraph__0{searchListOfListOfList{__typename ...on MovieResult{sections{__typename ...on EntityCollectionSection{__typename id}...on GallerySection{__typename id}}id}...on ArticleResult{id sections{__typename ...on GallerySection{__typename id}...on EntityCollectionSection{__typename id}}}}}", + "query":"query Search__searchSubgraph__0 { searchListOfListOfList { __typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { __typename id } fragment _generated_onGallerySection2_0 on GallerySection { __typename id } fragment _generated_onMovieResult2_0 on MovieResult { sections { __typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0 } id } fragment _generated_onArticleResult2_0 on ArticleResult { id sections { __typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0 } }", "operationName":"Search__searchSubgraph__0" }, "response": { @@ -137,4 +137,4 @@ } } ] -} \ No newline at end of file +} diff --git a/apollo-router/tests/integration/batching.rs b/apollo-router/tests/integration/batching.rs index 071ca5cb7a..7998a21528 100644 --- a/apollo-router/tests/integration/batching.rs +++ b/apollo-router/tests/integration/batching.rs @@ -147,6 +147,8 @@ async fn it_batches_with_errors_in_single_graph() -> Result<(), BoxError> { - errors: - message: expected error in A path: [] + extensions: + service: a - data: entryA: index: 2 @@ -200,9 +202,13 @@ async fn it_batches_with_errors_in_multi_graph() -> Result<(), BoxError> { - errors: - message: expected error in A path: [] + extensions: + service: a - errors: - message: expected error in B path: [] + extensions: + service: b - data: entryA: index: 2 @@ -256,6 +262,7 @@ async fn it_handles_short_timeouts() -> Result<(), BoxError> { path: [] extensions: code: REQUEST_TIMEOUT + service: b - data: entryA: index: 1 @@ -264,6 +271,7 @@ async fn it_handles_short_timeouts() -> Result<(), BoxError> { path: [] extensions: code: REQUEST_TIMEOUT + service: b "###); } @@ -331,16 +339,19 @@ async fn it_handles_indefinite_timeouts() -> Result<(), BoxError> { path: [] extensions: code: REQUEST_TIMEOUT + service: b - errors: - message: Request timed out path: [] extensions: code: REQUEST_TIMEOUT + service: b - errors: - message: Request timed out path: [] extensions: code: REQUEST_TIMEOUT + service: b "###); } @@ -568,6 +579,7 @@ async fn it_handles_cancelled_by_coprocessor() -> Result<(), BoxError> { path: [] extensions: code: ERR_NOT_ALLOWED + service: a - data: entryB: index: 0 @@ -576,6 +588,7 @@ async fn it_handles_cancelled_by_coprocessor() -> Result<(), BoxError> { path: [] extensions: code: ERR_NOT_ALLOWED + service: a - data: entryB: index: 1 @@ -725,6 +738,7 @@ async fn it_handles_single_request_cancelled_by_coprocessor() -> Result<(), BoxE path: [] extensions: code: ERR_NOT_ALLOWED + service: a - data: entryB: index: 2 diff --git a/apollo-router/tests/integration/file_upload.rs b/apollo-router/tests/integration/file_upload.rs index add9e81a90..fd272f9d28 100644 --- a/apollo-router/tests/integration/file_upload.rs +++ b/apollo-router/tests/integration/file_upload.rs @@ -22,6 +22,124 @@ macro_rules! make_handler { } } +#[tokio::test(flavor = "multi_thread")] +async fn it_uploads_file_to_subgraph() -> Result<(), BoxError> { + use reqwest::multipart::Form; + use reqwest::multipart::Part; + + const FILE: &str = "Hello, world!"; + const FILE_NAME: &str = "example.txt"; + + let request = Form::new() + .part( + "operations", + Part::text( + serde_json::json!({ + "query": "mutation SomeMutation($file: Upload) { + file: singleUpload(file: $file) { filename body } + }", + "variables": { "file": null }, + }) + .to_string(), + ), + ) + .part( + "map", + Part::text(serde_json::json!({ "0": ["variables.file"] }).to_string()), + ) + .part("0", Part::text(FILE).file_name(FILE_NAME)); + + async fn subgraph_handler( + mut request: http::Request, + ) -> impl axum::response::IntoResponse { + let boundary = request + .headers() + .get(CONTENT_TYPE) + .and_then(|v| multer::parse_boundary(v.to_str().ok()?).ok()) + .expect("subgraph request should have valid Content-Type header"); + let mut multipart = multer::Multipart::new(request.body_mut(), boundary); + + let operations_field = multipart + .next_field() + .await + .ok() + .flatten() + .expect("subgraph request should have valid `operations` field"); + assert_eq!(operations_field.name(), Some("operations")); + let operations: helper::Operation = + serde_json::from_slice(&operations_field.bytes().await.unwrap()).unwrap(); + insta::assert_json_snapshot!(operations, @r#" + { + "query": "mutation SomeMutation__uploads__0($file:Upload){file:singleUpload(file:$file){filename body}}", + "variables": { + "file": null + } + } + "#); + + let map_field = multipart + .next_field() + .await + .ok() + .flatten() + .expect("subgraph request should have valid `map` field"); + assert_eq!(map_field.name(), Some("map")); + let map: BTreeMap> = + serde_json::from_slice(&map_field.bytes().await.unwrap()).unwrap(); + insta::assert_json_snapshot!(map, @r#" + { + "0": [ + "variables.file" + ] + } + "#); + + let file_field = multipart + .next_field() + .await + .ok() + .flatten() + .expect("subgraph request should have file field"); + + ( + http::StatusCode::OK, + axum::Json(serde_json::json!({ + "data": { + "file": { + "filename": file_field.file_name().unwrap(), + "body": file_field.text().await.unwrap(), + }, + } + })), + ) + } + + // Run the test + helper::FileUploadTestServer::builder() + .config(FILE_CONFIG) + .handler(make_handler!(subgraph_handler)) + .request(request) + .subgraph_mapping("uploads", "/") + .build() + .run_test(|response| { + // FIXME: workaround to not update bellow snapshot if one of snapshots inside 'subgraph_handler' fails + // This would be fixed if subgraph shapshots are moved out of 'subgraph_handler' + assert_eq!(response.errors.len(), 0); + + insta::assert_json_snapshot!(response, @r###" + { + "data": { + "file": { + "filename": "example.txt", + "body": "Hello, world!" + } + } + } + "###); + }) + .await +} + #[tokio::test(flavor = "multi_thread")] async fn it_uploads_a_single_file() -> Result<(), BoxError> { const FILE: &str = "Hello, world!"; @@ -1043,7 +1161,7 @@ mod helper { pub body: Option, } - #[derive(Serialize, Deserialize)] + #[derive(Debug, Serialize, Deserialize)] pub struct Operation { // TODO: Can we verify that this is a valid graphql query? query: String, @@ -1168,10 +1286,8 @@ mod helper { return Err(FileUploadError::UnexpectedFile); } - let field_name: String = map - .into_keys() - .take(1) - .next() + let (field_name, _) = map + .first_key_value() .ok_or(FileUploadError::MissingMapping)?; // Extract the single expected file @@ -1181,7 +1297,7 @@ mod helper { .await? .ok_or(FileUploadError::MissingFile(field_name.clone()))?; - let file_name = f.file_name().unwrap_or(&field_name).to_string(); + let file_name = f.file_name().unwrap_or(field_name).to_string(); let body = f.bytes().await?; Upload { diff --git a/apollo-router/tests/integration/fixtures/query_planner_redis_config_update_query_fragments.router.yaml b/apollo-router/tests/integration/fixtures/query_planner_redis_config_update_query_fragments.router.yaml index ffb1e49152..e2232cb331 100644 --- a/apollo-router/tests/integration/fixtures/query_planner_redis_config_update_query_fragments.router.yaml +++ b/apollo-router/tests/integration/fixtures/query_planner_redis_config_update_query_fragments.router.yaml @@ -1,4 +1,4 @@ -# This config updates the query plan options so that we can see if there is a different redis cache entry generted for query plans +# This config updates the query plan options so that we can see if there is a different redis cache entry generated for query plans supergraph: query_planning: cache: @@ -7,5 +7,5 @@ supergraph: urls: - redis://localhost:6379 ttl: 10s - generate_query_fragments: true + generate_query_fragments: false diff --git a/apollo-router/tests/integration/mod.rs b/apollo-router/tests/integration/mod.rs index c383b5348f..7e775a21a9 100644 --- a/apollo-router/tests/integration/mod.rs +++ b/apollo-router/tests/integration/mod.rs @@ -12,6 +12,7 @@ mod operation_limits; mod operation_name; mod query_planner; mod subgraph_response; +mod supergraph; mod traffic_shaping; mod typename; diff --git a/apollo-router/tests/integration/query_planner.rs b/apollo-router/tests/integration/query_planner.rs index 03056ab47d..4b1c1ab70e 100644 --- a/apollo-router/tests/integration/query_planner.rs +++ b/apollo-router/tests/integration/query_planner.rs @@ -185,82 +185,8 @@ async fn context_with_new_qp() { .build() .await; router.start().await; - router - .assert_log_contains( - "could not create router: \ - failed to initialize the query planner: \ - `experimental_query_planner_mode: new` or `both` cannot yet \ - be used with `@context`. \ - Remove uses of `@context` to try the experimental query planner, \ - otherwise switch back to `legacy` or `both_best_effort`.", - ) - .await; - router.assert_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn context_with_legacy_qp_change_to_new_qp_keeps_old_config() { - if !graph_os_enabled() { - return; - } - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); - let mut router = IntegrationTest::builder() - .config(config) - .supergraph("tests/fixtures/set_context/supergraph.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{NEW_QP}"); - router.update_config(&config).await; - router - .assert_log_contains("error while reloading, continuing with previous configuration") - .await; - router - .assert_metrics_contains( - r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="context",init_is_success="false",otel_scope_name="apollo/router"} 1"#, - None, - ) - .await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn context_with_legacy_qp_reload_to_both_best_effort_keep_previous_config() { - if !graph_os_enabled() { - return; - } - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); - let mut router = IntegrationTest::builder() - .config(config) - .supergraph("tests/fixtures/set_context/supergraph.graphql") - .build() - .await; - router.start().await; router.assert_started().await; router.execute_default_query().await; - - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{BOTH_BEST_EFFORT_QP}"); - router.update_config(&config).await; - router - .assert_log_contains( - "Falling back to the legacy query planner: \ - failed to initialize the query planner: \ - `experimental_query_planner_mode: new` or `both` cannot yet \ - be used with `@context`. \ - Remove uses of `@context` to try the experimental query planner, \ - otherwise switch back to `legacy` or `both_best_effort`.", - ) - .await; - router - .assert_metrics_contains( - r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="context",init_is_success="false",otel_scope_name="apollo/router"} 1"#, - None, - ) - .await; - router.execute_default_query().await; router.graceful_shutdown().await; } diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index 15213ff1c4..955cbc9290 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -51,7 +51,7 @@ async fn query_planner_cache() -> Result<(), BoxError> { } // If this test fails and the cache key format changed you'll need to update the key here. // Look at the top of the file for instructions on getting the new cache key. - let known_cache_key = "plan:0:v2.9.3:70f115ebba5991355c17f4f56ba25bb093c519c4db49a30f3b10de279a4e3fa4:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:68e167191994b73c1892549ef57d0ec4cd76d518fad4dac5350846fe9af0b3f1"; + let known_cache_key = "plan:cache:1:federation:v2.9.3:70f115ebba5991355c17f4f56ba25bb093c519c4db49a30f3b10de279a4e3fa4:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:1cfc840090ac76a98f8bd51442f41fd6ca4c8d918b3f8d87894170745acf0734"; let config = RedisConfig::from_url("redis://127.0.0.1:6379").unwrap(); let client = RedisClient::new(config, None, None, None); @@ -325,7 +325,7 @@ async fn apq() -> Result<(), BoxError> { } #[tokio::test(flavor = "multi_thread")] -async fn entity_cache() -> Result<(), BoxError> { +async fn entity_cache_basic() -> Result<(), BoxError> { if !graph_os_enabled() { return Ok(()); } @@ -420,6 +420,10 @@ async fn entity_cache() -> Result<(), BoxError> { }, "include_subgraph_errors": { "all": true + }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, } })) .unwrap() @@ -534,6 +538,10 @@ async fn entity_cache() -> Result<(), BoxError> { }, "include_subgraph_errors": { "all": true + }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, } })) .unwrap() @@ -750,6 +758,10 @@ async fn entity_cache_authorization() -> Result<(), BoxError> { }, "include_subgraph_errors": { "all": true + }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, } })) .unwrap() @@ -962,8 +974,9 @@ async fn connection_failure_blocks_startup() { #[tokio::test(flavor = "multi_thread")] async fn query_planner_redis_update_query_fragments() { test_redis_query_plan_config_update( + // This configuration turns the fragment generation option *off*. include_str!("fixtures/query_planner_redis_config_update_query_fragments.router.yaml"), - "plan:0:v2.9.3:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:d239cf1d493e71f4bcb05e727c38e4cf55b32eb806791fa415bb6f6c8e5352e5", + "plan:cache:1:federation:v2.9.3:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:0ade8e18db172d9d51b36a2112513c15032d103100644df418a50596de3adfba", ) .await; } @@ -993,7 +1006,7 @@ async fn query_planner_redis_update_defer() { // test just passes locally. test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), - "plan:0:v2.9.3:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:752b870a0241594f54b7b593f16ab6cf6529eb5c9fe3d24e6bc4a618c24a5b81", + "plan:cache:1:federation:v2.9.3:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:066f41523274aed2428e0f08c9de077ee748a1d8470ec31edb5224030a198f3b", ) .await; } @@ -1015,7 +1028,7 @@ async fn query_planner_redis_update_type_conditional_fetching() { include_str!( "fixtures/query_planner_redis_config_update_type_conditional_fetching.router.yaml" ), - "plan:0:v2.9.3:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:e2145b320a44bebbd687c714dcfd046c032e56fe394aedcf50d9ab539f4354ea", + "plan:cache:1:federation:v2.9.3:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:b31d320db1af4015998cc89027f0ede2305dcc61724365e9b76d4252f90c7677", ) .await; } @@ -1037,7 +1050,7 @@ async fn query_planner_redis_update_reuse_query_fragments() { include_str!( "fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml" ), - "plan:0:v2.9.3:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:8b6c1838a55cbc6327adb5507f103eed1d5b1071e9acb9c67e098c5b9ea2887e", + "plan:cache:1:federation:v2.9.3:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:d54414eeede3a1bf631d88a84a1e3a354683be87746e79a69769cf18d919cc01", ) .await; } @@ -1061,8 +1074,7 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key router.assert_started().await; router.clear_redis_cache().await; - // If the tests above are failing, this is the key that needs to be changed first. - let starting_key = "plan:0:v2.9.3:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:41ae54204ebb1911412cf23e8f1d458cb08d6fabce16f255f7a497fd2b6fe213"; + let starting_key = "plan:cache:1:federation:v2.9.3:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:opname:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:metadata:1cfc840090ac76a98f8bd51442f41fd6ca4c8d918b3f8d87894170745acf0734"; assert_ne!(starting_key, new_cache_key, "starting_key (cache key for the initial config) and new_cache_key (cache key with the updated config) should not be equal. This either means that the cache key is not being generated correctly, or that the test is not actually checking the updated key."); router.execute_default_query().await; diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache-2.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache_basic-2.snap similarity index 100% rename from apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache-2.snap rename to apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache_basic-2.snap diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache-3.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache_basic-3.snap similarity index 100% rename from apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache-3.snap rename to apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache_basic-3.snap diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache-4.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache_basic-4.snap similarity index 100% rename from apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache-4.snap rename to apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache_basic-4.snap diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache-5.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache_basic-5.snap similarity index 100% rename from apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache-5.snap rename to apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache_basic-5.snap diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache_basic.snap similarity index 100% rename from apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache.snap rename to apollo-router/tests/integration/snapshots/integration_tests__integration__redis__entity_cache_basic.snap diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_rate_limit-2.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_rate_limit-2.snap index 07df294289..83a52acd05 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_rate_limit-2.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_rate_limit-2.snap @@ -2,4 +2,4 @@ source: apollo-router/tests/integration/traffic_shaping.rs expression: response --- -"{\"data\":null,\"errors\":[{\"message\":\"Your request has been rate limited\",\"path\":[],\"extensions\":{\"code\":\"REQUEST_RATE_LIMITED\"}}]}" +"{\"data\":null,\"errors\":[{\"message\":\"Your request has been rate limited\",\"path\":[],\"extensions\":{\"code\":\"REQUEST_RATE_LIMITED\",\"service\":\"products\"}}]}" diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_timeout.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_timeout.snap index 407674dfff..0364f2b734 100644 --- a/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_timeout.snap +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__traffic_shaping__subgraph_timeout.snap @@ -2,4 +2,4 @@ source: apollo-router/tests/integration/traffic_shaping.rs expression: response --- -"{\"data\":null,\"errors\":[{\"message\":\"Request timed out\",\"path\":[],\"extensions\":{\"code\":\"REQUEST_TIMEOUT\"}}]}" +"{\"data\":null,\"errors\":[{\"message\":\"Request timed out\",\"path\":[],\"extensions\":{\"code\":\"REQUEST_TIMEOUT\",\"service\":\"products\"}}]}" diff --git a/apollo-router/tests/integration/subgraph_response.rs b/apollo-router/tests/integration/subgraph_response.rs index 52fc56fa27..e37a0da067 100644 --- a/apollo-router/tests/integration/subgraph_response.rs +++ b/apollo-router/tests/integration/subgraph_response.rs @@ -81,6 +81,125 @@ async fn test_subgraph_returning_different_typename_on_query_root() -> Result<() Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn test_valid_extensions_service_for_subgraph_error() -> Result<(), BoxError> { + let mut router = IntegrationTest::builder() + .config(CONFIG) + .responder(ResponseTemplate::new(200).set_body_json(json!({ + "data": { "topProducts": null }, + "errors": [{ + "message": "Some error on subgraph", + "path": ["topProducts"] + }] + }))) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let (_trace_id, response) = router + .execute_query(&json!({ "query": "{ topProducts { name } }" })) + .await; + assert_eq!(response.status(), 200); + assert_eq!( + response.json::().await?, + json!({ + "data": { "topProducts": null }, + "errors": [{ + "message": "Some error on subgraph", + "path":["topProducts"], + "extensions": { + "service": "products" + } + }] + }) + ); + + router.graceful_shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_valid_extensions_service_is_preserved_for_subgraph_error() -> Result<(), BoxError> { + let mut router = IntegrationTest::builder() + .config(CONFIG) + .responder(ResponseTemplate::new(200).set_body_json(json!({ + "data": { "topProducts": null }, + "errors": [{ + "message": "Some error on subgraph", + "path": ["topProducts"], + "extensions": { + "service": 42, + } + }] + }))) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let (_trace_id, response) = router + .execute_query(&json!({ "query": "{ topProducts { name } }" })) + .await; + assert_eq!(response.status(), 200); + assert_eq!( + response.json::().await?, + json!({ + "data": { "topProducts": null }, + "errors": [{ + "message": "Some error on subgraph", + "path":["topProducts"], + "extensions": { + "service": 42, + } + }] + }) + ); + + router.graceful_shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_valid_extensions_service_for_invalid_subgraph_response() -> Result<(), BoxError> { + let mut router = IntegrationTest::builder() + .config(CONFIG) + .responder(ResponseTemplate::new(200)) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let (_trace_id, response) = router + .execute_query(&json!({ "query": "{ topProducts { name } }" })) + .await; + assert_eq!(response.status(), 200); + assert_eq!( + response.json::().await?, + json!({ + "data": null, + "errors": [ + { + "message": "HTTP fetch failed from 'products': subgraph response does not contain 'content-type' header; expected content-type: application/json or content-type: application/graphql-response+json", + "path": [], + "extensions": { + "code": "SUBREQUEST_HTTP_ERROR", + "service": "products", + "reason": "subgraph response does not contain 'content-type' header; expected content-type: application/json or content-type: application/graphql-response+json", + "http": { "status": 200 } + } + } + ] + }) + ); + + router.graceful_shutdown().await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn test_valid_error_locations() -> Result<(), BoxError> { let mut router = IntegrationTest::builder() @@ -116,7 +235,8 @@ async fn test_valid_error_locations() -> Result<(), BoxError> { { "line": 1, "column": 2 }, { "line": 3, "column": 4 }, ], - "path":["topProducts"] + "path":["topProducts"], + "extensions": { "service": "products" } }] }) ); @@ -153,7 +273,8 @@ async fn test_empty_error_locations() -> Result<(), BoxError> { "data": { "topProducts": null }, "errors": [{ "message":"Some error on subgraph", - "path":["topProducts"] + "path":["topProducts"], + "extensions": { "service": "products" } }] }) ); @@ -195,6 +316,7 @@ async fn test_invalid_error_locations() -> Result<(), BoxError> { "service": "products", "reason": "invalid `locations` within error: invalid type: boolean `true`, expected u32", "code": "SUBREQUEST_MALFORMED_RESPONSE", + "service": "products" } }] }) @@ -232,7 +354,8 @@ async fn test_invalid_error_locations_with_single_negative_one_location() -> Res "data": { "topProducts": null }, "errors": [{ "message":"Some error on subgraph", - "path":["topProducts"] + "path":["topProducts"], + "extensions": { "service": "products" } }] }) ); @@ -277,7 +400,8 @@ async fn test_invalid_error_locations_contains_negative_one_location() -> Result { "line": 1, "column": 2 }, { "line": 3, "column": 4 }, ], - "path":["topProducts"] + "path":["topProducts"], + "extensions": { "service": "products" } }] }) ); diff --git a/apollo-router/tests/integration/supergraph.rs b/apollo-router/tests/integration/supergraph.rs new file mode 100644 index 0000000000..97d5131d84 --- /dev/null +++ b/apollo-router/tests/integration/supergraph.rs @@ -0,0 +1,140 @@ +use std::collections::HashMap; + +use serde_json::json; +use tower::BoxError; + +use crate::integration::IntegrationTest; + +#[cfg(not(feature = "hyper_header_limits"))] +#[tokio::test(flavor = "multi_thread")] +async fn test_supergraph_error_http1_max_headers_config() -> Result<(), BoxError> { + let mut router = IntegrationTest::builder() + .config( + r#" + limits: + http1_max_request_headers: 100 + "#, + ) + .build() + .await; + + router.start().await; + router.assert_log_contains("'limits.http1_max_request_headers' requires 'hyper_header_limits' feature: enable 'hyper_header_limits' feature in order to use 'limits.http1_max_request_headers'").await; + router.assert_not_started().await; + Ok(()) +} + +#[cfg(feature = "hyper_header_limits")] +#[tokio::test(flavor = "multi_thread")] +async fn test_supergraph_errors_on_http1_max_headers() -> Result<(), BoxError> { + let mut router = IntegrationTest::builder() + .config( + r#" + limits: + http1_max_request_headers: 100 + "#, + ) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let mut headers = HashMap::new(); + for i in 0..100 { + headers.insert(format!("test-header-{i}"), format!("value_{i}")); + } + + let (_trace_id, response) = router + .execute_query_with_headers(&json!({ "query": "{ __typename }"}), headers) + .await; + assert_eq!(response.status(), 431); + Ok(()) +} + +#[cfg(feature = "hyper_header_limits")] +#[tokio::test(flavor = "multi_thread")] +async fn test_supergraph_allow_to_change_http1_max_headers() -> Result<(), BoxError> { + let mut router = IntegrationTest::builder() + .config( + r#" + limits: + http1_max_request_headers: 200 + "#, + ) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let mut headers = HashMap::new(); + for i in 0..100 { + headers.insert(format!("test-header-{i}"), format!("value_{i}")); + } + + let (_trace_id, response) = router + .execute_query_with_headers(&json!({ "query": "{ __typename }"}), headers) + .await; + assert_eq!(response.status(), 200); + assert_eq!( + response.json::().await?, + json!({ "data": { "__typename": "Query" } }) + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_supergraph_errors_on_http1_header_that_does_not_fit_inside_buffer( +) -> Result<(), BoxError> { + let mut router = IntegrationTest::builder() + .config( + r#" + limits: + http1_max_request_buf_size: 100kib + "#, + ) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let mut headers = HashMap::new(); + headers.insert("test-header".to_string(), "x".repeat(1048576 + 1)); + + let (_trace_id, response) = router + .execute_query_with_headers(&json!({ "query": "{ __typename }"}), headers) + .await; + assert_eq!(response.status(), 431); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_supergraph_allow_to_change_http1_max_buf_size() -> Result<(), BoxError> { + let mut router = IntegrationTest::builder() + .config( + r#" + limits: + http1_max_request_buf_size: 2mib + "#, + ) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let mut headers = HashMap::new(); + headers.insert("test-header".to_string(), "x".repeat(1048576 + 1)); + + let (_trace_id, response) = router + .execute_query_with_headers(&json!({ "query": "{ __typename }"}), headers) + .await; + assert_eq!(response.status(), 200); + assert_eq!( + response.json::().await?, + json!({ "data": { "__typename": "Query" } }) + ); + Ok(()) +} diff --git a/apollo-router/tests/integration/validation.rs b/apollo-router/tests/integration/validation.rs index b5b0f6682d..fa5ed5cdcf 100644 --- a/apollo-router/tests/integration/validation.rs +++ b/apollo-router/tests/integration/validation.rs @@ -167,3 +167,41 @@ async fn test_validation_error() { } "###); } + +#[tokio::test] +async fn test_lots_of_validation_errors() { + let query = format!( + "{{ __typename {} }}", + "@a".repeat(4_000), // Stay under the token limit: "@a" is two tokens every time + ); + + let request = serde_json::json!({ "query": query }); + let request = apollo_router::services::router::Request::fake_builder() + .body(request.to_string()) + .method(hyper::Method::POST) + .header("content-type", "application/json") + .build() + .unwrap(); + let response = apollo_router::TestHarness::builder() + .schema(include_str!("../fixtures/supergraph.graphql")) + .build_router() + .await + .unwrap() + .oneshot(request) + .await + .unwrap() + .next_response() + .await + .unwrap() + .unwrap(); + + let v: serde_json::Value = serde_json::from_slice(&response).unwrap(); + let errors = v["errors"].as_array().unwrap(); + assert!(!errors.is_empty(), "should have errors"); + // Make sure we're actually testing the validation errors, and we're not hitting some other limit + assert_eq!( + errors.first().unwrap()["extensions"]["code"], + serde_json::Value::from("GRAPHQL_VALIDATION_FAILED") + ); + assert!(errors.len() <= 100, "should return limited error count"); +} diff --git a/apollo-router/tests/samples/basic/interface-object/README.md b/apollo-router/tests/samples/basic/interface-object/README.md new file mode 100644 index 0000000000..a2e1d7fbb1 --- /dev/null +++ b/apollo-router/tests/samples/basic/interface-object/README.md @@ -0,0 +1,4 @@ + +# @interfaceObject + +Test the [`@interfaceObject`](https://www.apollographql.com/docs/federation/entities/interfaces/) directive. \ No newline at end of file diff --git a/apollo-router/tests/samples/basic/interface-object/configuration.yaml b/apollo-router/tests/samples/basic/interface-object/configuration.yaml new file mode 100644 index 0000000000..d5c60afffa --- /dev/null +++ b/apollo-router/tests/samples/basic/interface-object/configuration.yaml @@ -0,0 +1,7 @@ +override_subgraph_url: + products: http://localhost:4005 +include_subgraph_errors: + all: true + +plugins: + experimental.expose_query_plan: true \ No newline at end of file diff --git a/apollo-router/tests/samples/basic/interface-object/plan.json b/apollo-router/tests/samples/basic/interface-object/plan.json new file mode 100644 index 0000000000..91a5690a0c --- /dev/null +++ b/apollo-router/tests/samples/basic/interface-object/plan.json @@ -0,0 +1,254 @@ +{ + "actions": [ + { + "type": "Start", + "schema_path": "./supergraph.graphql", + "configuration_path": "./configuration.yaml", + "subgraphs": { + "accounts": { + "requests": [ + { + "request": { + "body": { + "query": "query TestItf__accounts__0{i{__typename id x ...on A{a}...on B{b}}}", + "operationName": "TestItf__accounts__0" + } + }, + "response": { + "body": { + "data": { + "i": [ + { + "__typename": "A", + "id": "1", + "x": 1, + "a": "a" + }, + null, + { + "__typename": "B", + "id": "2", + "x": 2, + "b": "b" + }, + { + "__typename": "A", + "id": "1", + "x": 1, + "a": "a" + }, + { + "__typename": "B", + "id": "3", + "x": 3, + "b": "c" + } + ] + } + } + } + } + ] + }, + "products": { + "requests": [ + { + "request": { + "body": { + "query": "query TestItf__products__1($representations:[_Any!]!){_entities(representations:$representations){...on I{y}}}", + "operationName": "TestItf__products__1", + "variables": { + "representations": [ + { + "__typename": "I", + "id": "1" + }, + { + "__typename": "I", + "id": "2" + }, + { + "__typename": "I", + "id": "3" + } + ] + } + } + }, + "response": { + "body": { + "data": { + "_entities": [ + { + "y": 1 + }, + { + "y": 2 + }, + null + ] + } + } + } + } + ] + }, + "reviews": { + "requests": [] + } + } + }, + { + "type": "Request", + "request": { + "query": "query TestItf { i { __typename x y ... on A { a } ... on B { b } } }" + }, + "expected_response": { + "data": { + "i": [ + { + "__typename": "A", + "x": 1, + "y": 1, + "a": "a" + }, + null, + { + "__typename": "B", + "x": 2, + "y": 2, + "b": "b" + }, + { + "__typename": "A", + "x": 1, + "y": 1, + "a": "a" + }, + { + "__typename": "B", + "x": 3, + "y": null, + "b": "c" + } + ] + } + } + }, + { + "type": "ReloadSubgraphs", + "subgraphs": { + "accounts": { + "requests": [ + { + "request": { + "body": { + "query": "query TestItf2__accounts__0{req{__typename id i{__typename id x}}}", + "operationName": "TestItf2__accounts__0" + } + }, + "response": { + "body": { + "data": { + "req": { + "__typename": "C", + "id": "1", + "i": { + "__typename": "A", + "id": "1", + "x": 1 + } + } + } + } + } + } + ] + }, + "products": { + "requests": [ + { + "request": { + "body": { + "query": "query TestItf2__products__1($representations:[_Any!]!){_entities(representations:$representations){...on I{y}}}", + "operationName": "TestItf2__products__1", + "variables": { + "representations": [ + { + "__typename": "I", + "id": "1" + } + ] + } + } + }, + "response": { + "body": { + "data": { + "_entities": [ + { + "y": 1 + } + ] + } + } + } + } + ] + }, + "reviews": { + "requests": [ + { + "request": { + "body": { + "query": "query TestItf2__reviews__2($representations:[_Any!]!){_entities(representations:$representations){...on C{c}}}", + "operationName": "TestItf2__reviews__2", + "variables": { + "representations": [ + { + "__typename": "C", + "i": { + "x": 1, + "y": 1 + }, + "id": "1" + } + ] + } + } + }, + "response": { + "body": { + "data": { + "_entities": [ + { + "c": "c" + } + ] + } + } + } + } + ] + } + } + }, + { + "type": "Request", + "request": { + "query": "query TestItf2 { req { id c } }" + }, + "expected_response": { + "data": { + "req": { + "id": "1", + "c": "c" + } + } + } + }, + { + "type": "Stop" + } + ] +} \ No newline at end of file diff --git a/apollo-router/tests/samples/basic/interface-object/supergraph.graphql b/apollo-router/tests/samples/basic/interface-object/supergraph.graphql new file mode 100644 index 0000000000..08783bf70f --- /dev/null +++ b/apollo-router/tests/samples/basic/interface-object/supergraph.graphql @@ -0,0 +1,115 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query +} + +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar join__FieldSet +scalar link__Import + +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: REVIEWS) { + i: [I] @join__field(graph: ACCOUNTS) + req: C @join__field(graph: ACCOUNTS) +} + +interface I + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: PRODUCTS, key: "id", isInterfaceObject: true) + @join__type(graph: REVIEWS) { + id: ID! + x: Int @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS) + y: Int @join__field(graph: PRODUCTS) @join__field(graph: REVIEWS) +} + +type A implements I + @join__type(graph: ACCOUNTS, key: "id") + @join__implements(graph: ACCOUNTS, interface: "I") { + id: ID! + x: Int + y: Int @join__field + a: String +} + +type B implements I + @join__type(graph: ACCOUNTS, key: "id") + @join__implements(graph: ACCOUNTS, interface: "I") { + id: ID! + x: Int + y: Int @join__field + b: String +} + +type C + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! + i: I @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS, external: true) + c: String @join__field(graph: REVIEWS, requires: "i { x y }") +} \ No newline at end of file diff --git a/apollo-router/tests/samples/basic/query1/README.md b/apollo-router/tests/samples/core/defer/README.md similarity index 100% rename from apollo-router/tests/samples/basic/query1/README.md rename to apollo-router/tests/samples/core/defer/README.md diff --git a/apollo-router/tests/samples/basic/query1/configuration.yaml b/apollo-router/tests/samples/core/defer/configuration.yaml similarity index 100% rename from apollo-router/tests/samples/basic/query1/configuration.yaml rename to apollo-router/tests/samples/core/defer/configuration.yaml diff --git a/apollo-router/tests/samples/core/defer/plan.json b/apollo-router/tests/samples/core/defer/plan.json new file mode 100644 index 0000000000..72dd5efed0 --- /dev/null +++ b/apollo-router/tests/samples/core/defer/plan.json @@ -0,0 +1,106 @@ +{ + "actions": [ + { + "type": "Start", + "schema_path": "./supergraph.graphql", + "configuration_path": "./configuration.yaml", + "subgraphs": { + "accounts": { + "requests": [ + { + "request": { + "body": { + "query": "{me{__typename name id}}" + } + }, + "response": { + "body": { + "data": { + "me": { + "__typename": "User", + "name": "test", + "id": "1" + } + } + } + } + } + ] + }, + "reviews": { + "requests": [ + { + "request": { + "body": { + "query": "query($representations:[_Any!]!){_entities(representations:$representations){..._generated_onUser1_0}}fragment _generated_onUser1_0 on User{reviews{body}}", + "variables": { + "representations": [ + { + "__typename": "User", + "id": "1" + } + ] + } + } + }, + "response": { + "body": { + "data": { + "_entities": [ + { + "reviews": [ + { + "body": "Test" + } + ] + } + ] + } + } + } + } + ] + } + } + }, + { + "type": "Request", + "headers": { + "Accept": "multipart/mixed;deferSpec=20220824" + }, + "request": { + "query": "{ me { name ... @defer { reviews { body } } } }" + }, + "expected_response": [ + { + "data": { + "me": { + "name": "test" + } + }, + "hasNext": true + }, + { + "hasNext": false, + "incremental": [ + { + "data": { + "reviews": [ + { + "body": "Test" + } + ] + }, + "path": [ + "me" + ] + } + ] + } + ] + }, + { + "type": "Stop" + } + ] +} diff --git a/apollo-router/tests/samples/basic/query2/supergraph.graphql b/apollo-router/tests/samples/core/defer/supergraph.graphql similarity index 100% rename from apollo-router/tests/samples/basic/query2/supergraph.graphql rename to apollo-router/tests/samples/core/defer/supergraph.graphql diff --git a/apollo-router/tests/samples/basic/query2/README.md b/apollo-router/tests/samples/core/query1/README.md similarity index 100% rename from apollo-router/tests/samples/basic/query2/README.md rename to apollo-router/tests/samples/core/query1/README.md diff --git a/apollo-router/tests/samples/basic/query2/configuration.yaml b/apollo-router/tests/samples/core/query1/configuration.yaml similarity index 100% rename from apollo-router/tests/samples/basic/query2/configuration.yaml rename to apollo-router/tests/samples/core/query1/configuration.yaml diff --git a/apollo-router/tests/samples/basic/query1/plan.json b/apollo-router/tests/samples/core/query1/plan.json similarity index 100% rename from apollo-router/tests/samples/basic/query1/plan.json rename to apollo-router/tests/samples/core/query1/plan.json diff --git a/apollo-router/tests/samples/basic/query1/supergraph.graphql b/apollo-router/tests/samples/core/query1/supergraph.graphql similarity index 100% rename from apollo-router/tests/samples/basic/query1/supergraph.graphql rename to apollo-router/tests/samples/core/query1/supergraph.graphql diff --git a/apollo-router/tests/samples/core/query2/README.md b/apollo-router/tests/samples/core/query2/README.md new file mode 100644 index 0000000000..9386489fb0 --- /dev/null +++ b/apollo-router/tests/samples/core/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/core/query2/configuration.yaml b/apollo-router/tests/samples/core/query2/configuration.yaml new file mode 100644 index 0000000000..f7ed04641e --- /dev/null +++ b/apollo-router/tests/samples/core/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/core/query2/plan.json similarity index 100% rename from apollo-router/tests/samples/basic/query2/plan.json rename to apollo-router/tests/samples/core/query2/plan.json diff --git a/apollo-router/tests/samples/core/query2/supergraph.graphql b/apollo-router/tests/samples/core/query2/supergraph.graphql new file mode 100644 index 0000000000..1bd9f596ee --- /dev/null +++ b/apollo-router/tests/samples/core/query2/supergraph.graphql @@ -0,0 +1,125 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query + mutation: Mutation +} + +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +scalar link__Import + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) +} + +type Product + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible + name: String @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) + reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + upc: String! +} + +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { + me: User @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) +} + +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String @join__field(graph: REVIEWS) + author: User @join__field(graph: REVIEWS, provides: "username") + product: Product @join__field(graph: REVIEWS) +} + +type User + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! + name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + reviews: [Review] @join__field(graph: REVIEWS) +} diff --git a/apollo-router/tests/samples/enterprise/entity-cache/defer/README.md b/apollo-router/tests/samples/enterprise/entity-cache/defer/README.md new file mode 100644 index 0000000000..a96d350b73 --- /dev/null +++ b/apollo-router/tests/samples/enterprise/entity-cache/defer/README.md @@ -0,0 +1,3 @@ +# Entity cache with @defer + +This tests `Cache-Control` aggregation when using the `@defer` directive. \ No newline at end of file diff --git a/apollo-router/tests/samples/enterprise/entity-cache/defer/configuration.yaml b/apollo-router/tests/samples/enterprise/entity-cache/defer/configuration.yaml new file mode 100644 index 0000000000..fb6b95ecd4 --- /dev/null +++ b/apollo-router/tests/samples/enterprise/entity-cache/defer/configuration.yaml @@ -0,0 +1,23 @@ +override_subgraph_url: + products: http://localhost:4005 +include_subgraph_errors: + all: true + +preview_entity_cache: + enabled: true + redis: + urls: + ["redis://localhost:6379",] + subgraph: + all: + enabled: true + subgraphs: + reviews: + ttl: 120s + enabled: true + +telemetry: + exporters: + logging: + stdout: + format: text \ No newline at end of file diff --git a/apollo-router/tests/samples/enterprise/entity-cache/defer/plan.json b/apollo-router/tests/samples/enterprise/entity-cache/defer/plan.json new file mode 100644 index 0000000000..83a777d329 --- /dev/null +++ b/apollo-router/tests/samples/enterprise/entity-cache/defer/plan.json @@ -0,0 +1,113 @@ +{ + "enterprise": true, + "redis": true, + "actions": [ + { + "type": "Start", + "schema_path": "./supergraph.graphql", + "configuration_path": "./configuration.yaml", + "subgraphs": { + "cache-defer-accounts": { + "requests": [ + { + "request": { + "body": { + "query": "query CacheDefer__cache_defer_accounts__0{me{__typename name id}}", + "operationName": "CacheDefer__cache_defer_accounts__0" + } + }, + "response": { + "headers": { + "Cache-Control": "public, max-age=10", + "Content-Type": "application/json" + }, + "body": { + "data": { + "me": { + "__typename": "User", + "name": "test-user", + "id": "1" + } + } + } + } + } + ] + }, + "cache-defer-reviews": { + "requests": [ + { + "request": { + "body": { + "query": "query CacheDefer__cache_defer_reviews__1($representations:[_Any!]!){_entities(representations:$representations){..._generated_onUser1_0}}fragment _generated_onUser1_0 on User{reviews{body}}", + "operationName": "CacheDefer__cache_defer_reviews__1", + "variables": { + "representations": [ + { + "id": "1", + "__typename": "User" + } + ] + } + } + }, + "response": { + "headers": { + "Cache-Control": "public, max-age=100", + "Content-Type": "application/json" + }, + "body": { + "data": { + "reviews": [ + { + "body": "test-review" + } + ] + } + } + } + } + ] + } + } + }, + { + "type": "Request", + "request": { + "query": "query CacheDefer { me { name ... @defer { reviews { body } } } }" + }, + "headers": { + "Accept": "multipart/mixed;deferSpec=20220824" + }, + "expected_response": [ + { + "data": { + "me": { + "name": "test-user" + } + }, + "hasNext": true + }, + { + "hasNext": false, + "incremental": [ + { + "data": { + "reviews": null + }, + "path": [ + "me" + ] + } + ] + } + ], + "expected_headers": { + "Cache-Control": "max-age=10,public" + } + }, + { + "type": "Stop" + } + ] +} diff --git a/apollo-router/tests/samples/enterprise/entity-cache/defer/supergraph.graphql b/apollo-router/tests/samples/enterprise/entity-cache/defer/supergraph.graphql new file mode 100644 index 0000000000..320a9c2a70 --- /dev/null +++ b/apollo-router/tests/samples/enterprise/entity-cache/defer/supergraph.graphql @@ -0,0 +1,122 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/tag/v0.3") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query + mutation: Mutation +} + +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar join__FieldSet +scalar link__Import + +enum join__Graph { + ACCOUNTS @join__graph(name: "cache-defer-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: "cache-defer-reviews", url: "https://reviews.demo.starstuff.dev") +} + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) + @join__type(graph: ACCOUNTS) { + updateMyAccount: User @join__field(graph: ACCOUNTS) + createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) +} + +type Product + @join__type(graph: ACCOUNTS, key: "upc", extension: true) + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible + name: String @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) + reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + upc: String! +} + +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { + me: User @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) +} + +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + author: User @join__field(graph: REVIEWS, provides: "username") + body: String + product: Product +} + +type User + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! + name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + reviews: [Review] @join__field(graph: REVIEWS) +} diff --git a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/configuration.yaml b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/configuration.yaml index e283bbdace..47f1e99e06 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/configuration.yaml +++ b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/configuration.yaml @@ -5,12 +5,12 @@ include_subgraph_errors: preview_entity_cache: enabled: true - redis: - urls: - ["redis://localhost:6379",] subgraph: all: enabled: true + redis: + urls: + ["redis://localhost:6379",] subgraphs: invalidation-entity-key-reviews: ttl: 120s diff --git a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/plan.json b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/plan.json index 1bb1bc0210..c8588ed2b7 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/plan.json +++ b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/plan.json @@ -31,7 +31,7 @@ { "request": { "body": { - "query":"query InvalidationEntityKey__invalidation_entity_key_reviews__1($representations:[_Any!]!){_entities(representations:$representations){...on Product{reviews{body}}}}", + "query":"query InvalidationEntityKey__invalidation_entity_key_reviews__1($representations:[_Any!]!){_entities(representations:$representations){..._generated_onProduct1_0}}fragment _generated_onProduct1_0 on Product{reviews{body}}", "operationName": "InvalidationEntityKey__invalidation_entity_key_reviews__1", "variables":{"representations":[{"upc":"0","__typename":"Product"},{"upc":"1","__typename":"Product"}]} } @@ -115,7 +115,7 @@ { "request": { "body": { - "query":"query InvalidationEntityKey__invalidation_entity_key_reviews__1($representations:[_Any!]!){_entities(representations:$representations){...on Product{reviews{body}}}}", + "query":"query InvalidationEntityKey__invalidation_entity_key_reviews__1($representations:[_Any!]!){_entities(representations:$representations){..._generated_onProduct1_0}}fragment _generated_onProduct1_0 on Product{reviews{body}}", "variables":{"representations":[{"upc":"1","__typename":"Product"}]} } }, @@ -202,7 +202,7 @@ { "request": { "body": { - "query":"query InvalidationEntityKey__invalidation_entity_key_reviews__1($representations:[_Any!]!){_entities(representations:$representations){...on Product{reviews{body}}}}", + "query":"query InvalidationEntityKey__invalidation_entity_key_reviews__1($representations:[_Any!]!){_entities(representations:$representations){..._generated_onProduct1_0}}fragment _generated_onProduct1_0 on Product{reviews{body}}", "variables":{"representations":[{"upc":"1","__typename":"Product"}]} } }, @@ -250,4 +250,4 @@ "type": "Stop" } ] -} \ No newline at end of file +} diff --git a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/configuration.yaml b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/configuration.yaml index 85e106df9f..7efcac9a81 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/configuration.yaml +++ b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/configuration.yaml @@ -8,12 +8,12 @@ preview_entity_cache: invalidation: listen: 127.0.0.1:4000 path: /invalidation - redis: - urls: - ["redis://localhost:6379",] subgraph: all: enabled: true + redis: + urls: + ["redis://localhost:6379",] subgraphs: reviews: ttl: 120s diff --git a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/configuration.yaml b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/configuration.yaml index 96577bbb28..8378e3b127 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/configuration.yaml +++ b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/configuration.yaml @@ -5,9 +5,6 @@ include_subgraph_errors: preview_entity_cache: enabled: true - redis: - urls: - ["redis://localhost:6379",] invalidation: # FIXME: right now we cannot configure it to use the same port used for the GraphQL endpoint if it is chosen at random listen: 127.0.0.1:12345 @@ -15,6 +12,9 @@ preview_entity_cache: subgraph: all: enabled: true + redis: + urls: + ["redis://localhost:6379",] invalidation: enabled: true shared_key: "1234" diff --git a/apollo-router/tests/samples_tests.rs b/apollo-router/tests/samples_tests.rs index cf2e9b99f7..8159a40179 100644 --- a/apollo-router/tests/samples_tests.rs +++ b/apollo-router/tests/samples_tests.rs @@ -14,6 +14,9 @@ use std::process::ExitCode; use libtest_mimic::Arguments; use libtest_mimic::Failed; use libtest_mimic::Trial; +use mediatype::MediaTypeList; +use mediatype::ReadParams; +use multer::Multipart; use serde::Deserialize; use serde_json::Value; use tokio::runtime::Runtime; @@ -173,12 +176,14 @@ impl TestExecution { query_path, headers, expected_response, + expected_headers, } => { self.request( request.clone(), query_path.as_deref(), headers, expected_response, + expected_headers, path, out, ) @@ -421,12 +426,14 @@ impl TestExecution { } } + #[allow(clippy::too_many_arguments)] async fn request( &mut self, mut request: Value, query_path: Option<&str>, headers: &HashMap, expected_response: &Value, + expected_headers: &HashMap, path: &Path, out: &mut String, ) -> Result<(), Failed> { @@ -450,19 +457,107 @@ impl TestExecution { } writeln!(out, "query: {}\n", serde_json::to_string(&request).unwrap()).unwrap(); + writeln!(out, "header: {:?}\n", headers).unwrap(); + let (_, response) = router .execute_query_with_headers(&request, headers.clone()) .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(); + writeln!(out, "response headers: {:?}", response.headers()).unwrap(); + + let mut failed = false; + for (key, value) in expected_headers { + if !response.headers().contains_key(key) { + failed = true; + writeln!(out, "expected header {} to be present", key).unwrap(); + } else if response.headers().get(key).unwrap() != value { + failed = true; + writeln!( + out, + "expected header {} to be {}, got {:?}", + key, + value, + response.headers().get(key).unwrap() + ) + .unwrap(); + } + } + if failed { let f: Failed = out.clone().into(); - f - })?; + return Err(f); + } + + let content_type = response + .headers() + .get("content-type") + .unwrap() + .to_str() + .unwrap(); + let mut is_multipart = false; + let mut boundary = None; + for mime in MediaTypeList::new(content_type).flatten() { + if mime.ty == mediatype::names::MULTIPART && mime.subty == mediatype::names::MIXED { + is_multipart = true; + boundary = mime.get_param(mediatype::names::BOUNDARY).map(|v| { + // multer does not strip quotes from the boundary: https://github.com/rwf2/multer/issues/64 + let mut s = v.as_str(); + if s.starts_with('\"') && s.ends_with('\"') { + s = &s[1..s.len() - 1]; + } + + s.to_string() + }); + } + } + + let graphql_response: Value = if !is_multipart { + 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 + })?; + serde_json::from_slice(&body).map_err(|e| { + writeln!( + out, + "could not deserialize graphql response data: {e}\nfrom:\n{}", + std::str::from_utf8(&body).unwrap() + ) + .unwrap(); + let f: Failed = out.clone().into(); + f + })? + } else { + let mut chunks = Vec::new(); + + let mut multipart = Multipart::new(response.bytes_stream(), boundary.unwrap()); + + // Iterate over the fields, use `next_field()` to get the next field. + while let Some(mut field) = multipart.next_field().await.map_err(|e| { + writeln!(out, "could not get next field from multipart body: {e}",).unwrap(); + let f: Failed = out.clone().into(); + f + })? { + while let Some(chunk) = field.chunk().await.map_err(|e| { + writeln!(out, "could not get next chunk from multipart body: {e}",).unwrap(); + let f: Failed = out.clone().into(); + f + })? { + writeln!(out, "multipart chunk: {:?}\n", std::str::from_utf8(&chunk)).unwrap(); + + let parsed: Value = serde_json::from_slice(&chunk).map_err(|e| { + writeln!( + out, + "could not deserialize graphql response data: {e}\nfrom:\n{}", + std::str::from_utf8(&chunk).unwrap() + ) + .unwrap(); + let f: Failed = out.clone().into(); + f + })?; + chunks.push(parsed); + } + } + Value::Array(chunks) + }; if expected_response != &graphql_response { if let Some(requests) = self @@ -599,6 +694,8 @@ enum Action { #[serde(default)] headers: HashMap, expected_response: Value, + #[serde(default)] + expected_headers: HashMap, }, EndpointRequest { url: url::Url, diff --git a/apollo-router/tests/set_context.rs b/apollo-router/tests/set_context.rs index 47d8deb076..dc3f366fcf 100644 --- a/apollo-router/tests/set_context.rs +++ b/apollo-router/tests/set_context.rs @@ -32,9 +32,10 @@ macro_rules! snap } } -async fn run_single_request(query: &str, mocks: &[(&'static str, &'static str)]) -> Response { - let harness = setup_from_mocks( - json! {{ +fn get_configuration(rust_qp: bool) -> serde_json::Value { + if rust_qp { + return json! {{ + "experimental_query_planner_mode": "new", "experimental_type_conditioned_fetching": true, // will make debugging easier "plugins": { @@ -42,10 +43,36 @@ async fn run_single_request(query: &str, mocks: &[(&'static str, &'static str)]) }, "include_subgraph_errors": { "all": true + }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, } - }}, - mocks, - ); + }}; + } + json! {{ + "experimental_type_conditioned_fetching": true, + // will make debugging easier + "plugins": { + "experimental.expose_query_plan": true + }, + "include_subgraph_errors": { + "all": true + }, + "supergraph": { + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "generate_query_fragments": false, + } + }} +} + +async fn run_single_request( + query: &str, + rust_qp: bool, + mocks: &[(&'static str, &'static str)], +) -> Response { + let configuration = get_configuration(rust_qp); + let harness = setup_from_mocks(configuration, mocks); let supergraph_service = harness.build_supergraph().await.unwrap(); let request = supergraph::Request::fake_builder() .query(query.to_string()) @@ -79,6 +106,34 @@ async fn test_set_context() { let response = run_single_request( QUERY, + false, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_rust_qp() { + static QUERY: &str = r#" + query set_context_rust_qp { + t { + __typename + id + u { + __typename + field + } + } + }"#; + + let response = run_single_request( + QUERY, + true, &[ ("Subgraph1", include_str!("fixtures/set_context/one.json")), ("Subgraph2", include_str!("fixtures/set_context/two.json")), @@ -103,6 +158,32 @@ async fn test_set_context_no_typenames() { let response = run_single_request( QUERY_NO_TYPENAMES, + false, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_no_typenames_rust_qp() { + static QUERY_NO_TYPENAMES: &str = r#" + query set_context_no_typenames_rust_qp { + t { + id + u { + field + } + } + }"#; + + let response = run_single_request( + QUERY_NO_TYPENAMES, + true, &[ ("Subgraph1", include_str!("fixtures/set_context/one.json")), ("Subgraph2", include_str!("fixtures/set_context/two.json")), @@ -127,6 +208,32 @@ async fn test_set_context_list() { let response = run_single_request( QUERY_WITH_LIST, + false, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_list_rust_qp() { + static QUERY_WITH_LIST: &str = r#" + query set_context_list_rust_qp { + t { + id + uList { + field + } + } + }"#; + + let response = run_single_request( + QUERY_WITH_LIST, + true, &[ ("Subgraph1", include_str!("fixtures/set_context/one.json")), ("Subgraph2", include_str!("fixtures/set_context/two.json")), @@ -151,6 +258,32 @@ async fn test_set_context_list_of_lists() { let response = run_single_request( QUERY_WITH_LIST_OF_LISTS, + false, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_list_of_lists_rust_qp() { + static QUERY_WITH_LIST_OF_LISTS: &str = r#" + query QueryLL { + tList { + id + uList { + field + } + } + }"#; + + let response = run_single_request( + QUERY_WITH_LIST_OF_LISTS, + true, &[ ("Subgraph1", include_str!("fixtures/set_context/one.json")), ("Subgraph2", include_str!("fixtures/set_context/two.json")), @@ -181,6 +314,38 @@ async fn test_set_context_union() { let response = run_single_request( QUERY_WITH_UNION, + false, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_union_rust_qp() { + static QUERY_WITH_UNION: &str = r#" + query QueryUnion { + k { + ... on A { + v { + field + } + } + ... on B { + v { + field + } + } + } + }"#; + + let response = run_single_request( + QUERY_WITH_UNION, + true, &[ ("Subgraph1", include_str!("fixtures/set_context/one.json")), ("Subgraph2", include_str!("fixtures/set_context/two.json")), @@ -205,6 +370,35 @@ async fn test_set_context_with_null() { let response = run_single_request( QUERY, + false, + &[ + ( + "Subgraph1", + include_str!("fixtures/set_context/one_null_param.json"), + ), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + insta::assert_json_snapshot!(response); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_with_null_rust_qp() { + static QUERY: &str = r#" + query Query_Null_Param { + t { + id + u { + field + } + } + }"#; + + let response = run_single_request( + QUERY, + true, &[ ( "Subgraph1", @@ -234,6 +428,34 @@ async fn test_set_context_type_mismatch() { let response = run_single_request( QUERY, + false, + &[ + ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +// this test returns the contextual value with a different than expected type +// this currently works, but perhaps should do type valdiation in the future to reject +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_type_mismatch_rust_qp() { + static QUERY: &str = r#" + query Query_type_mismatch { + t { + id + u { + field + } + } + }"#; + + let response = run_single_request( + QUERY, + true, &[ ("Subgraph1", include_str!("fixtures/set_context/one.json")), ("Subgraph2", include_str!("fixtures/set_context/two.json")), @@ -261,6 +483,38 @@ async fn test_set_context_unrelated_fetch_failure() { let response = run_single_request( QUERY, + false, + &[ + ( + "Subgraph1", + include_str!("fixtures/set_context/one_fetch_failure.json"), + ), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +// fetch from unrelated (to context) subgraph fails +// validates that the error propagation is correct +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_unrelated_fetch_failure_rust_qp() { + static QUERY: &str = r#" + query Query_fetch_failure { + t { + id + u { + field + b + } + } + }"#; + + let response = run_single_request( + QUERY, + true, &[ ( "Subgraph1", @@ -290,6 +544,37 @@ async fn test_set_context_dependent_fetch_failure() { let response = run_single_request( QUERY, + false, + &[ + ( + "Subgraph1", + include_str!("fixtures/set_context/one_dependent_fetch_failure.json"), + ), + ("Subgraph2", include_str!("fixtures/set_context/two.json")), + ], + ) + .await; + + snap!(response); +} + +// subgraph fetch fails where context depends on results of fetch. +// validates that no fetch will get called that passes context +#[tokio::test(flavor = "multi_thread")] +async fn test_set_context_dependent_fetch_failure_rust_qp() { + static QUERY: &str = r#" + query Query_fetch_dependent_failure { + t { + id + u { + field + } + } + }"#; + + let response = run_single_request( + QUERY, + true, &[ ( "Subgraph1", diff --git a/apollo-router/tests/snapshots/apollo_otel_traces__condition_else-2.snap b/apollo-router/tests/snapshots/apollo_otel_traces__condition_else-2.snap index 853672e8d0..602929d475 100644 --- a/apollo-router/tests/snapshots/apollo_otel_traces__condition_else-2.snap +++ b/apollo-router/tests/snapshots/apollo_otel_traces__condition_else-2.snap @@ -271,7 +271,7 @@ resourceSpans: stringValue: "[redacted]" - key: apollo_private.operation_signature value: - stringValue: "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if,label:\"\"){reviews{author{name}}reviews{author{name}}}}}" + stringValue: "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if){reviews{author{name}}reviews{author{name}}}}}" - key: apollo_private.query.aliases value: intValue: 0 diff --git a/apollo-router/tests/snapshots/apollo_otel_traces__condition_else.snap b/apollo-router/tests/snapshots/apollo_otel_traces__condition_else.snap index 853672e8d0..602929d475 100644 --- a/apollo-router/tests/snapshots/apollo_otel_traces__condition_else.snap +++ b/apollo-router/tests/snapshots/apollo_otel_traces__condition_else.snap @@ -271,7 +271,7 @@ resourceSpans: stringValue: "[redacted]" - key: apollo_private.operation_signature value: - stringValue: "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if,label:\"\"){reviews{author{name}}reviews{author{name}}}}}" + stringValue: "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if){reviews{author{name}}reviews{author{name}}}}}" - key: apollo_private.query.aliases value: intValue: 0 diff --git a/apollo-router/tests/snapshots/apollo_otel_traces__condition_if-2.snap b/apollo-router/tests/snapshots/apollo_otel_traces__condition_if-2.snap index 1b4bcb3cf1..69ea7edf5b 100644 --- a/apollo-router/tests/snapshots/apollo_otel_traces__condition_if-2.snap +++ b/apollo-router/tests/snapshots/apollo_otel_traces__condition_if-2.snap @@ -271,7 +271,7 @@ resourceSpans: stringValue: "[redacted]" - key: apollo_private.operation_signature value: - stringValue: "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if,label:\"\"){reviews{author{name}}reviews{author{name}}}}}" + stringValue: "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if){reviews{author{name}}reviews{author{name}}}}}" - key: apollo_private.query.aliases value: intValue: 0 diff --git a/apollo-router/tests/snapshots/apollo_otel_traces__condition_if.snap b/apollo-router/tests/snapshots/apollo_otel_traces__condition_if.snap index 1b4bcb3cf1..69ea7edf5b 100644 --- a/apollo-router/tests/snapshots/apollo_otel_traces__condition_if.snap +++ b/apollo-router/tests/snapshots/apollo_otel_traces__condition_if.snap @@ -271,7 +271,7 @@ resourceSpans: stringValue: "[redacted]" - key: apollo_private.operation_signature value: - stringValue: "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if,label:\"\"){reviews{author{name}}reviews{author{name}}}}}" + stringValue: "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if){reviews{author{name}}reviews{author{name}}}}}" - key: apollo_private.query.aliases value: intValue: 0 diff --git a/apollo-router/tests/snapshots/apollo_reports__condition_else-2.snap b/apollo-router/tests/snapshots/apollo_reports__condition_else-2.snap index cae3641fd5..8fd3f3243e 100644 --- a/apollo-router/tests/snapshots/apollo_reports__condition_else-2.snap +++ b/apollo-router/tests/snapshots/apollo_reports__condition_else-2.snap @@ -11,7 +11,7 @@ header: uname: "[uname]" executable_schema_id: "[executable_schema_id]" traces_per_query: - "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if,label:\"\"){reviews{author{name}}reviews{author{name}}}}}": + "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if){reviews{author{name}}reviews{author{name}}}}}": trace: - start_time: seconds: "[seconds]" diff --git a/apollo-router/tests/snapshots/apollo_reports__condition_else.snap b/apollo-router/tests/snapshots/apollo_reports__condition_else.snap index cae3641fd5..8fd3f3243e 100644 --- a/apollo-router/tests/snapshots/apollo_reports__condition_else.snap +++ b/apollo-router/tests/snapshots/apollo_reports__condition_else.snap @@ -11,7 +11,7 @@ header: uname: "[uname]" executable_schema_id: "[executable_schema_id]" traces_per_query: - "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if,label:\"\"){reviews{author{name}}reviews{author{name}}}}}": + "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if){reviews{author{name}}reviews{author{name}}}}}": trace: - start_time: seconds: "[seconds]" diff --git a/apollo-router/tests/snapshots/apollo_reports__condition_if-2.snap b/apollo-router/tests/snapshots/apollo_reports__condition_if-2.snap index b9d955ee9c..282dbbb14d 100644 --- a/apollo-router/tests/snapshots/apollo_reports__condition_if-2.snap +++ b/apollo-router/tests/snapshots/apollo_reports__condition_if-2.snap @@ -11,7 +11,7 @@ header: uname: "[uname]" executable_schema_id: "[executable_schema_id]" traces_per_query: - "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if,label:\"\"){reviews{author{name}}reviews{author{name}}}}}": + "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if){reviews{author{name}}reviews{author{name}}}}}": trace: - start_time: seconds: "[seconds]" diff --git a/apollo-router/tests/snapshots/apollo_reports__condition_if.snap b/apollo-router/tests/snapshots/apollo_reports__condition_if.snap index b9d955ee9c..282dbbb14d 100644 --- a/apollo-router/tests/snapshots/apollo_reports__condition_if.snap +++ b/apollo-router/tests/snapshots/apollo_reports__condition_if.snap @@ -11,7 +11,7 @@ header: uname: "[uname]" executable_schema_id: "[executable_schema_id]" traces_per_query: - "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if,label:\"\"){reviews{author{name}}reviews{author{name}}}}}": + "# -\nquery($if:Boolean!){topProducts{name...@defer(if:$if){reviews{author{name}}reviews{author{name}}}}}": trace: - start_time: seconds: "[seconds]" diff --git a/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap index 4c44587cdd..67390167e7 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap @@ -16,7 +16,10 @@ expression: response "path": [ "t", "u" - ] + ], + "extensions": { + "service": "Subgraph1" + } } ], "extensions": { diff --git a/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure_rust_qp.snap new file mode 100644 index 0000000000..5caea23420 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure_rust_qp.snap @@ -0,0 +1,115 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": null, + "errors": [ + { + "message": "Some error", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": [ + "t", + "u" + ], + "extensions": { + "service": "Subgraph1" + } + } + ], + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_fetch_dependent_failure__Subgraph1__0 { t { __typename prop id u { __typename id } } }", + "operationKind": "query", + "operationName": "Query_fetch_dependent_failure__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "6b659295c8e5aff7b3d7146b878e848b43ad58fba3f4dfce2988530631c3448a", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_fetch_dependent_failure__Subgraph1__1($representations: [_Any!]!, $contextualArgument_1_0: String) { _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0) } } }", + "operationKind": "query", + "operationName": "Query_fetch_dependent_failure__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "3bc84712c95d01c4e9118cc1f8179e071662862a04cef56d39a0ac6a621daf36", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "t", + "u" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \"t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + }, + "valueCompletion": [ + { + "message": "Cannot return null for non-nullable field Query.t", + "path": [] + } + ] + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_list_of_lists_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_list_of_lists_rust_qp.snap new file mode 100644 index 0000000000..0945ccf058 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_list_of_lists_rust_qp.snap @@ -0,0 +1,112 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "tList": [ + { + "id": "1", + "uList": [ + { + "field": 3456 + } + ] + }, + { + "id": "2", + "uList": [ + { + "field": 4567 + } + ] + } + ] + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query QueryLL__Subgraph1__0 { tList { __typename prop id uList { __typename id } } }", + "operationKind": "query", + "operationName": "QueryLL__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "0a6255094b34a44c5addf88a5a9bb37847f19ecf10370be675ba55a1330b4ac7", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query QueryLL__Subgraph1__1($representations: [_Any!]!, $contextualArgument_1_0: String) { _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0) } } }", + "operationKind": "query", + "operationName": "QueryLL__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "71e6d73b679197d0e979c07446c670bad69897d77bd280dc9c39276fde6e8d99", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "tList", + "@", + "uList", + "@" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n tList {\n __typename\n prop\n id\n uList {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \"tList.@.uList.@\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_list_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_list_rust_qp.snap new file mode 100644 index 0000000000..ab643923c0 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_list_rust_qp.snap @@ -0,0 +1,107 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "t": { + "id": "1", + "uList": [ + { + "field": 1234 + }, + { + "field": 2345 + }, + { + "field": 3456 + } + ] + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query set_context_list_rust_qp__Subgraph1__0 { t { __typename prop id uList { __typename id } } }", + "operationKind": "query", + "operationName": "set_context_list_rust_qp__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "fd215e24828b3a7296abe901f843f68b525d8eaf35a019ac34a2198738c91230", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query set_context_list_rust_qp__Subgraph1__1($representations: [_Any!]!, $contextualArgument_1_0: String) { _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0) } } }", + "operationKind": "query", + "operationName": "set_context_list_rust_qp__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "96a8bf16a86cbddab93bb46364f8b0e63635a928924dcb681dc2371b810eee02", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "t", + "uList", + "@" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n uList {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \"t.uList.@\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_no_typenames_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_no_typenames_rust_qp.snap new file mode 100644 index 0000000000..6a47496428 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_no_typenames_rust_qp.snap @@ -0,0 +1,98 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "t": { + "id": "1", + "u": { + "field": 1234 + } + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query set_context_no_typenames_rust_qp__Subgraph1__0 { t { __typename prop id u { __typename id } } }", + "operationKind": "query", + "operationName": "set_context_no_typenames_rust_qp__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "9c1d7c67821fc43d63e8a217417fbe600a9100e1a43ba50e2f961d4fd4974144", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query set_context_no_typenames_rust_qp__Subgraph1__1($representations: [_Any!]!, $contextualArgument_1_0: String) { _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0) } } }", + "operationKind": "query", + "operationName": "set_context_no_typenames_rust_qp__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "5fdc56a38428bad98d0c5e46f096c0179886815509ffc1918f5c6e0a784e2547", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "t", + "u" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \"t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_rust_qp.snap new file mode 100644 index 0000000000..fecc966894 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_rust_qp.snap @@ -0,0 +1,100 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "t": { + "__typename": "T", + "id": "1", + "u": { + "__typename": "U", + "field": 1234 + } + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query set_context_rust_qp__Subgraph1__0 { t { __typename prop id u { __typename id } } }", + "operationKind": "query", + "operationName": "set_context_rust_qp__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "7fb5b477b89d2dcf76239dd30abcf6210462e144376a6b1b589ceb603edd55cd", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query set_context_rust_qp__Subgraph1__1($representations: [_Any!]!, $contextualArgument_1_0: String) { _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0) } } }", + "operationKind": "query", + "operationName": "set_context_rust_qp__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "fef499e9ca815057242c5a03e9f0960d5c50d6958b0ac7329fc23b5a6e714eab", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "t", + "u" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \"t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_type_mismatch_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_type_mismatch_rust_qp.snap new file mode 100644 index 0000000000..516c42f5fe --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_type_mismatch_rust_qp.snap @@ -0,0 +1,98 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "t": { + "id": "1", + "u": { + "field": 1234 + } + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_type_mismatch__Subgraph1__0 { t { __typename prop id u { __typename id } } }", + "operationKind": "query", + "operationName": "Query_type_mismatch__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "29f5e6a254fac05382ddc3e4aac47368dc9847abe711ecf17dbfca7945097faf", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_type_mismatch__Subgraph1__1($representations: [_Any!]!, $contextualArgument_1_0: String) { _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0) } } }", + "operationKind": "query", + "operationName": "Query_type_mismatch__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "864f2eecd06e2c450e48f2cb551d4e95946575eb7e537a17a04c9a1716c0a482", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "t", + "u" + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \"t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_union_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_union_rust_qp.snap new file mode 100644 index 0000000000..74e41f53c9 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_union_rust_qp.snap @@ -0,0 +1,173 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "k": { + "v": { + "field": 3456 + } + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query QueryUnion__Subgraph1__0 { k { __typename ... on A { __typename prop v { __typename id } } ... on B { __typename prop v { __typename id } } } }", + "operationKind": "query", + "operationName": "QueryUnion__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "eae4d791b0314c4e2509735ad3d0dd0ca5de8ee4c7f315513931df6e4cb5102d", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on A", + "prop" + ], + "renameKeyTo": "contextualArgument_1_1" + }, + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on B", + "prop" + ], + "renameKeyTo": "contextualArgument_1_1" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query QueryUnion__Subgraph1__1($representations: [_Any!]!, $contextualArgument_1_1: String) { _entities(representations: $representations) { ... on V { field(a: $contextualArgument_1_1) } } }", + "operationKind": "query", + "operationName": "QueryUnion__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "V" + } + ], + "schemaAwareHash": "b1ba6dd8a0e2edc415efd084401bfa01ecbaaa76a0f7896c27c431bed8c20a08", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_1" + ] + }, + "path": [ + "k|[B]", + "v" + ] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on A", + "prop" + ], + "renameKeyTo": "contextualArgument_1_1" + }, + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on B", + "prop" + ], + "renameKeyTo": "contextualArgument_1_1" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query QueryUnion__Subgraph1__2($representations: [_Any!]!, $contextualArgument_1_1: String) { _entities(representations: $representations) { ... on V { field(a: $contextualArgument_1_1) } } }", + "operationKind": "query", + "operationName": "QueryUnion__Subgraph1__2", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "V" + } + ], + "schemaAwareHash": "45785998d1610758abe68519f9bc100828afa2ba56c7e55b9d18ad69f3ad27eb", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_1" + ] + }, + "path": [ + "k|[A]", + "v" + ] + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n k {\n __typename\n ... on A {\n __typename\n prop\n v {\n __typename\n id\n }\n }\n ... on B {\n __typename\n prop\n v {\n __typename\n id\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \"k|[B].v\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on V {\n __typename\n id\n }\n } =>\n {\n ... on V {\n field(a: $contextualArgument_1_1)\n }\n }\n },\n },\n Flatten(path: \"k|[A].v\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on V {\n __typename\n id\n }\n } =>\n {\n ... on V {\n field(a: $contextualArgument_1_1)\n }\n }\n },\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure.snap b/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure.snap index 49dcf6bf9b..ead3b10258 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure.snap @@ -10,7 +10,10 @@ expression: response "path": [ "t", "u" - ] + ], + "extensions": { + "service": "Subgraph2" + } } ], "extensions": { diff --git a/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure_rust_qp.snap new file mode 100644 index 0000000000..8194cfc237 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_unrelated_fetch_failure_rust_qp.snap @@ -0,0 +1,172 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +snapshot_kind: text +--- +{ + "data": null, + "errors": [ + { + "message": "Some error", + "path": [ + "t", + "u" + ], + "extensions": { + "service": "Subgraph2" + } + } + ], + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_fetch_failure__Subgraph1__0 { t { __typename prop id u { __typename id } } }", + "operationKind": "query", + "operationName": "Query_fetch_failure__Subgraph1__0", + "outputRewrites": null, + "schemaAwareHash": "d3f1ad875170d008059583ca6074e732a178f74474ac31de3bb4397c5080020d", + "serviceName": "Subgraph1", + "variableUsages": [] + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_fetch_failure__Subgraph1__1($representations: [_Any!]!, $contextualArgument_1_0: String) { _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0) } } }", + "operationKind": "query", + "operationName": "Query_fetch_failure__Subgraph1__1", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "05dc59a826cec26ad8263101508c298dd8d31d79d36f18194dd4cf8cd5f02dc3", + "serviceName": "Subgraph1", + "variableUsages": [ + "contextualArgument_1_0" + ] + }, + "path": [ + "t", + "u" + ] + }, + { + "kind": "Flatten", + "node": { + "authorization": { + "is_authenticated": false, + "policies": [], + "scopes": [] + }, + "contextRewrites": null, + "id": null, + "inputRewrites": null, + "kind": "Fetch", + "operation": "query Query_fetch_failure__Subgraph2__2($representations: [_Any!]!) { _entities(representations: $representations) { ... on U { b } } }", + "operationKind": "query", + "operationName": "Query_fetch_failure__Subgraph2__2", + "outputRewrites": null, + "requires": [ + { + "kind": "InlineFragment", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ], + "typeCondition": "U" + } + ], + "schemaAwareHash": "a3c7e6df6f9c93b228f16a937b7159ccf1294fec50a92f60ba004dbebbb64b50", + "serviceName": "Subgraph2", + "variableUsages": [] + }, + "path": [ + "t", + "u" + ] + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Parallel {\n Flatten(path: \"t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n Flatten(path: \"t.u\") {\n Fetch(service: \"Subgraph2\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n b\n }\n }\n },\n },\n },\n },\n}" + }, + "valueCompletion": [ + { + "message": "Cannot return null for non-nullable field U.field", + "path": [ + "t", + "u" + ] + }, + { + "message": "Cannot return null for non-nullable field T.u", + "path": [ + "t", + "u" + ] + }, + { + "message": "Cannot return null for non-nullable field T!.t", + "path": [ + "t" + ] + } + ] + } +} diff --git a/apollo-router/tests/snapshots/set_context__set_context_with_null_rust_qp.snap b/apollo-router/tests/snapshots/set_context__set_context_with_null_rust_qp.snap new file mode 100644 index 0000000000..6f775414e3 --- /dev/null +++ b/apollo-router/tests/snapshots/set_context__set_context_with_null_rust_qp.snap @@ -0,0 +1,98 @@ +--- +source: apollo-router/tests/set_context.rs +expression: response +--- +{ + "data": { + "t": { + "id": "1", + "u": { + "field": 1234 + } + } + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "Subgraph1", + "variableUsages": [], + "operation": "query Query_Null_Param__Subgraph1__0 { t { __typename prop id u { __typename id } } }", + "operationName": "Query_Null_Param__Subgraph1__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "4fc423a49bbddcc8869c014934dfd128dd61a1760c4eb619940ad46f614c843b", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + }, + { + "kind": "Flatten", + "path": [ + "t", + "u" + ], + "node": { + "kind": "Fetch", + "serviceName": "Subgraph1", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "U", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "contextualArgument_1_0" + ], + "operation": "query Query_Null_Param__Subgraph1__1($representations: [_Any!]!, $contextualArgument_1_0: String) { _entities(representations: $representations) { ... on U { field(a: $contextualArgument_1_0) } } }", + "operationName": "Query_Null_Param__Subgraph1__1", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": [ + { + "kind": "KeyRenamer", + "path": [ + "..", + "... on T", + "prop" + ], + "renameKeyTo": "contextualArgument_1_0" + } + ], + "schemaAwareHash": "d863b0ef9ef616faaade4c73b2599395e074ec1521ec07634471894145e97f44", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n prop\n id\n u {\n __typename\n id\n }\n }\n }\n },\n Flatten(path: \"t.u\") {\n Fetch(service: \"Subgraph1\") {\n {\n ... on U {\n __typename\n id\n }\n } =>\n {\n ... on U {\n field(a: $contextualArgument_1_0)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap index 17f0ec285c..390a7c79df 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap @@ -72,14 +72,14 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operation": "query Search__searchSubgraph__0 { search { __typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { __typename id } fragment _generated_onGallerySection2_0 on GallerySection { __typename id } fragment _generated_onMovieResult2_0 on MovieResult { sections { __typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0 } id } fragment _generated_onArticleResult2_0 on ArticleResult { id sections { __typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0 } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "587c887350ef75eaf4b647be94fd682616bcd33909e15fb797cee226e95fa36a", + "schemaAwareHash": "70ca85b28e861b24a7749862930a5f965c4c6e8074d60a87a3952d596fe7cc36", "authorization": { "is_authenticated": false, "scopes": [], @@ -130,14 +130,14 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "622eb49d4e52ff2636348e103f941d04b783fec97de59d0ae6635d9272f97ad8", + "schemaAwareHash": "317a722a677563080aeac92f60ac2257d9288ca6851a0e8980fcf18f58b462a8", "authorization": { "is_authenticated": false, "scopes": [], @@ -148,7 +148,7 @@ expression: response ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Flatten(path: \"search.@.sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n\n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n\n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n\n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Flatten(path: \"search.@.sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n },\n}" } } } diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap index f355685192..687028717f 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap @@ -72,14 +72,14 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operation": "query Search__searchSubgraph__0 { search { __typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { __typename id } fragment _generated_onGallerySection2_0 on GallerySection { __typename id } fragment _generated_onMovieResult2_0 on MovieResult { sections { __typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0 } id } fragment _generated_onArticleResult2_0 on ArticleResult { id sections { __typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0 } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "5201830580c9c5fadd9c59aea072878f84465c1ae9d905207fa281aa7c1d5340", + "schemaAwareHash": "0e1644746fe4beab7def35ec8cc12bde39874c6bb8b9dfd928456196b814a111", "authorization": { "is_authenticated": false, "scopes": [], @@ -130,14 +130,14 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "62ff891f6971184d3e42b98f8166be72027b5479f9ec098af460a48ea6f6cbf4", + "schemaAwareHash": "6510f6b9672829bd9217618b78ef6f329fbddb125f88184d04e6faaa982ff8bb", "authorization": { "is_authenticated": false, "scopes": [], @@ -148,7 +148,7 @@ expression: response ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Flatten(path: \"search.@.sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n \n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n \n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n \n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Flatten(path: \"search.@.sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n },\n}" } } } diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap index 01e49a0721..ed94ed7a85 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap @@ -72,14 +72,14 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operation": "query Search__searchSubgraph__0 { search { __typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { __typename id } fragment _generated_onGallerySection2_0 on GallerySection { __typename id } fragment _generated_onMovieResult2_0 on MovieResult { sections { __typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0 } id } fragment _generated_onArticleResult2_0 on ArticleResult { id sections { __typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0 } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "587c887350ef75eaf4b647be94fd682616bcd33909e15fb797cee226e95fa36a", + "schemaAwareHash": "70ca85b28e861b24a7749862930a5f965c4c6e8074d60a87a3952d596fe7cc36", "authorization": { "is_authenticated": false, "scopes": [], @@ -133,14 +133,14 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "a0bf36d3a611df53c3a60b9b124a2887f2d266858221c606ace0985d101d64bd", + "schemaAwareHash": "1d21a65a3b5a31e17f7834750ef5b37fb49d99d0a1e2145f00a62d43c5f8423a", "authorization": { "is_authenticated": false, "scopes": [], @@ -192,14 +192,14 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "3e84a53f967bf40d4c08254a94f3fa32a828ab3ad8184a22bb3439c596ecaaf4", + "schemaAwareHash": "df321f6532c2c9eda0d8c042e5f08073c24e558dd0cae01054886b79416a6c08", "authorization": { "is_authenticated": false, "scopes": [], @@ -212,7 +212,7 @@ expression: response ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \"search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n Flatten(path: \"search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n\n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n\n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n\n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \"search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n Flatten(path: \"search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n },\n },\n}" } } } diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap index a8afd1fa0e..08a9782c85 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap @@ -72,14 +72,14 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operation": "query Search__searchSubgraph__0 { search { __typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { __typename id } fragment _generated_onGallerySection2_0 on GallerySection { __typename id } fragment _generated_onMovieResult2_0 on MovieResult { sections { __typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0 } id } fragment _generated_onArticleResult2_0 on ArticleResult { id sections { __typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0 } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "5201830580c9c5fadd9c59aea072878f84465c1ae9d905207fa281aa7c1d5340", + "schemaAwareHash": "0e1644746fe4beab7def35ec8cc12bde39874c6bb8b9dfd928456196b814a111", "authorization": { "is_authenticated": false, "scopes": [], @@ -134,14 +134,14 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "62ff891f6971184d3e42b98f8166be72027b5479f9ec098af460a48ea6f6cbf4", + "schemaAwareHash": "6510f6b9672829bd9217618b78ef6f329fbddb125f88184d04e6faaa982ff8bb", "authorization": { "is_authenticated": false, "scopes": [], @@ -194,14 +194,14 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "7e6f6850777335eb1421a30a45f6888bb9e5d0acf8f55d576d55d1c4b7d23ec7", + "schemaAwareHash": "6bc34c108f7cf81896971bffad76dc5275d46231b4dfe492ccc205dda9a4aa16", "authorization": { "is_authenticated": false, "scopes": [], @@ -214,7 +214,7 @@ expression: response ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \".search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n Flatten(path: \".search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n \n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n \n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n \n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \".search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n Flatten(path: \".search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n },\n },\n}" } } } diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap index c990725153..fc5007829d 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap @@ -134,14 +134,14 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0 { searchListOfList { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operation": "query Search__searchSubgraph__0 { searchListOfList { __typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { __typename id } fragment _generated_onGallerySection2_0 on GallerySection { __typename id } fragment _generated_onMovieResult2_0 on MovieResult { sections { __typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0 } id } fragment _generated_onArticleResult2_0 on ArticleResult { id sections { __typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0 } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "365685f6b9f1c5dd02506b27f50d63486f1ca6b5ced7b0253fc050ef73732e03", + "schemaAwareHash": "ff18ff586aee784ec507117854cb4b64f9693d528df1ee69c922b5d75ae637fb", "authorization": { "is_authenticated": false, "scopes": [], @@ -196,14 +196,14 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "a0bf36d3a611df53c3a60b9b124a2887f2d266858221c606ace0985d101d64bd", + "schemaAwareHash": "1d21a65a3b5a31e17f7834750ef5b37fb49d99d0a1e2145f00a62d43c5f8423a", "authorization": { "is_authenticated": false, "scopes": [], @@ -256,14 +256,14 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "3e84a53f967bf40d4c08254a94f3fa32a828ab3ad8184a22bb3439c596ecaaf4", + "schemaAwareHash": "df321f6532c2c9eda0d8c042e5f08073c24e558dd0cae01054886b79416a6c08", "authorization": { "is_authenticated": false, "scopes": [], @@ -276,7 +276,7 @@ expression: response ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfList {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \"searchListOfList.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n Flatten(path: \"searchListOfList.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfList {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n\n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n\n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n\n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \"searchListOfList.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n Flatten(path: \"searchListOfList.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n },\n },\n}" } } } diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap index adf981893a..4c219874d6 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap @@ -134,14 +134,14 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0 { searchListOfList { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operation": "query Search__searchSubgraph__0 { searchListOfList { __typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { __typename id } fragment _generated_onGallerySection2_0 on GallerySection { __typename id } fragment _generated_onMovieResult2_0 on MovieResult { sections { __typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0 } id } fragment _generated_onArticleResult2_0 on ArticleResult { id sections { __typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0 } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "51a7aadec14b66d9f6c737be7418bac0be1af89fcc55dac55d9e9b125bc3682d", + "schemaAwareHash": "70b62e564b3924984694d90de2b10947a2f5c14ceb76d154f43bb3c638c4830b", "authorization": { "is_authenticated": false, "scopes": [], @@ -197,14 +197,14 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "62ff891f6971184d3e42b98f8166be72027b5479f9ec098af460a48ea6f6cbf4", + "schemaAwareHash": "6510f6b9672829bd9217618b78ef6f329fbddb125f88184d04e6faaa982ff8bb", "authorization": { "is_authenticated": false, "scopes": [], @@ -258,14 +258,14 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "7e6f6850777335eb1421a30a45f6888bb9e5d0acf8f55d576d55d1c4b7d23ec7", + "schemaAwareHash": "6bc34c108f7cf81896971bffad76dc5275d46231b4dfe492ccc205dda9a4aa16", "authorization": { "is_authenticated": false, "scopes": [], @@ -278,7 +278,7 @@ expression: response ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfList {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \".searchListOfList.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n Flatten(path: \".searchListOfList.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfList {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n \n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n \n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n \n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \".searchListOfList.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n Flatten(path: \".searchListOfList.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n },\n },\n}" } } } diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap index f206e94c89..5cc97759df 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap @@ -138,14 +138,14 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0 { searchListOfListOfList { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operation": "query Search__searchSubgraph__0 { searchListOfListOfList { __typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { __typename id } fragment _generated_onGallerySection2_0 on GallerySection { __typename id } fragment _generated_onMovieResult2_0 on MovieResult { sections { __typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0 } id } fragment _generated_onArticleResult2_0 on ArticleResult { id sections { __typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0 } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "47d7643dd7226529814a57e0382aafb3790c2d8a8b26354aa2f60e9c9f097a05", + "schemaAwareHash": "cb374f6eaa19cb529eeae258f2b136dbc751e3784fdc279954e59622cfb1edde", "authorization": { "is_authenticated": false, "scopes": [], @@ -201,14 +201,14 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "a0bf36d3a611df53c3a60b9b124a2887f2d266858221c606ace0985d101d64bd", + "schemaAwareHash": "1d21a65a3b5a31e17f7834750ef5b37fb49d99d0a1e2145f00a62d43c5f8423a", "authorization": { "is_authenticated": false, "scopes": [], @@ -262,14 +262,14 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "3e84a53f967bf40d4c08254a94f3fa32a828ab3ad8184a22bb3439c596ecaaf4", + "schemaAwareHash": "df321f6532c2c9eda0d8c042e5f08073c24e558dd0cae01054886b79416a6c08", "authorization": { "is_authenticated": false, "scopes": [], @@ -282,7 +282,7 @@ expression: response ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfListOfList {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \"searchListOfListOfList.@.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n Flatten(path: \"searchListOfListOfList.@.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfListOfList {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n\n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n\n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n\n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \"searchListOfListOfList.@.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n Flatten(path: \"searchListOfListOfList.@.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n },\n },\n}" } } } diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap index 78a539ec02..593bd573f6 100644 --- a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap @@ -138,14 +138,14 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0 { searchListOfListOfList { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operation": "query Search__searchSubgraph__0 { searchListOfListOfList { __typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { __typename id } fragment _generated_onGallerySection2_0 on GallerySection { __typename id } fragment _generated_onMovieResult2_0 on MovieResult { sections { __typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0 } id } fragment _generated_onArticleResult2_0 on ArticleResult { id sections { __typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0 } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "e6f45a784fb669930586f13fc587f55798089a87ee4b23a7d1736e0516367a6a", + "schemaAwareHash": "26ae1da614855e4edee344061c0fc95ec4613a99e012de1f33207cb5318487b8", "authorization": { "is_authenticated": false, "scopes": [], @@ -202,14 +202,14 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "62ff891f6971184d3e42b98f8166be72027b5479f9ec098af460a48ea6f6cbf4", + "schemaAwareHash": "6510f6b9672829bd9217618b78ef6f329fbddb125f88184d04e6faaa982ff8bb", "authorization": { "is_authenticated": false, "scopes": [], @@ -264,14 +264,14 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "7e6f6850777335eb1421a30a45f6888bb9e5d0acf8f55d576d55d1c4b7d23ec7", + "schemaAwareHash": "6bc34c108f7cf81896971bffad76dc5275d46231b4dfe492ccc205dda9a4aa16", "authorization": { "is_authenticated": false, "scopes": [], @@ -284,7 +284,7 @@ expression: response ] } }, - "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfListOfList {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \".searchListOfListOfList.@.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n Flatten(path: \".searchListOfListOfList.@.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n },\n },\n}" + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfListOfList {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n \n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n \n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n \n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \".searchListOfListOfList.@.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n Flatten(path: \".searchListOfListOfList.@.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n \n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n },\n },\n}" } } } diff --git a/apollo-router/tests/tracing_common/mod.rs b/apollo-router/tests/tracing_common/mod.rs index 0eda97f796..f7f698b87f 100644 --- a/apollo-router/tests/tracing_common/mod.rs +++ b/apollo-router/tests/tracing_common/mod.rs @@ -426,7 +426,15 @@ pub(crate) fn subgraph_mocks(subgraph: &str) -> subgraph::BoxService { }; builder.with_json( json!({ - "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on Product{reviews{author{__typename id}}}}}", + "query": " + query($representations: [_Any!]!) { + _entities(representations: $representations) { + ..._generated_onProduct1_0 + } + } + fragment _generated_onProduct1_0 on Product { + reviews { author{ __typename id } } + }", "variables": {"representations": [ {"__typename": "Product", "upc": "1"}, {"__typename": "Product", "upc": "2"}, diff --git a/apollo-router/tests/type_conditions.rs b/apollo-router/tests/type_conditions.rs index 88757228b8..ab43b3f521 100644 --- a/apollo-router/tests/type_conditions.rs +++ b/apollo-router/tests/type_conditions.rs @@ -118,9 +118,6 @@ async fn _test_type_conditions_enabled_generate_query_fragments(planner_mode: &s json! {{ "experimental_type_conditioned_fetching": true, "experimental_query_planner_mode": planner_mode, - "supergraph": { - "generate_query_fragments": true - }, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -321,6 +318,10 @@ async fn _test_type_conditions_enabled_shouldnt_make_article_fetch(planner_mode: "plugins": { "experimental.expose_query_plan": true }, + // TODO(@goto-bus-stop): need to update the mocks and remove this, #6013 + "supergraph": { + "generate_query_fragments": false, + }, "include_subgraph_errors": { "all": true } diff --git a/deny.toml b/deny.toml index 915ca9e58c..55cd3fb0f0 100644 --- a/deny.toml +++ b/deny.toml @@ -53,7 +53,8 @@ allow = [ "MIT", "MPL-2.0", "Elastic-2.0", - "Unicode-DFS-2016" + "Unicode-DFS-2016", + "Zlib" ] copyleft = "warn" allow-osi-fsf-free = "neither" diff --git a/dockerfiles/tracing/docker-compose.datadog.yml b/dockerfiles/tracing/docker-compose.datadog.yml index db46f878fb..e69604eb8c 100644 --- a/dockerfiles/tracing/docker-compose.datadog.yml +++ b/dockerfiles/tracing/docker-compose.datadog.yml @@ -3,7 +3,7 @@ services: apollo-router: container_name: apollo-router - image: ghcr.io/apollographql/router:v1.57.1 + image: ghcr.io/apollographql/router:v1.58.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/datadog.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.jaeger.yml b/dockerfiles/tracing/docker-compose.jaeger.yml index a38d04b6ba..8225d7799a 100644 --- a/dockerfiles/tracing/docker-compose.jaeger.yml +++ b/dockerfiles/tracing/docker-compose.jaeger.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router #build: ./router - image: ghcr.io/apollographql/router:v1.57.1 + image: ghcr.io/apollographql/router:v1.58.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/jaeger.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml index 366bff5506..a84a7f1fcf 100644 --- a/dockerfiles/tracing/docker-compose.zipkin.yml +++ b/dockerfiles/tracing/docker-compose.zipkin.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router build: ./router - image: ghcr.io/apollographql/router:v1.57.1 + image: ghcr.io/apollographql/router:v1.58.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/zipkin.router.yaml:/etc/config/configuration.yaml diff --git a/docs/shared/coproc-typical-config.mdx b/docs/shared/coproc-typical-config.mdx index a9812d47c3..5986426fd7 100644 --- a/docs/shared/coproc-typical-config.mdx +++ b/docs/shared/coproc-typical-config.mdx @@ -38,10 +38,12 @@ coprocessor: uri: false method: false service_name: false + subgraph_request_id: false response: # By including this key, the `SubgraphService` sends a coprocessor request whenever receives a subgraph response. headers: true body: false context: false service_name: false status_code: false + subgraph_request_id: false ``` diff --git a/docs/source/enterprise-features.mdx b/docs/source/enterprise-features.mdx deleted file mode 100644 index 4691048016..0000000000 --- a/docs/source/enterprise-features.mdx +++ /dev/null @@ -1,183 +0,0 @@ ---- -title: GraphOS Router Enterprise Features -subtitle: Use expanded features of GraphOS Router enabled by an Enterprise plan -description: Unlock Enterprise features for the GraphOS Router by connecting it to Apollo GraphOS. Enable offline licenses for extended disconnections. ---- - -GraphOS organizations with the Enterprise plan can [connect a self-hosted router to GraphOS](/graphos/quickstart/self-hosted#6-connect-the-router-to-graphos) for an expanded feature set. - -A router connected to GraphOS, whether cloud- or self-hosted, is called a **GraphOS Router**. It has access to specific GraphOS features depending on the connected GraphOS organization's plan. Refer to the [pricing page](https://www.apollographql.com/pricing#graphos-router) to compare GraphOS Router features across plan types. - - - -Try the Enterprise features of GraphOS Router for free with an [Enterprise trial](/graphos/org/plans/#enterprise-trials). - - - -## List of features - - - - - -Articles about Enterprise features are marked with a **❖** icon in the left navigation. - - - -For details on these features, see [this blog post](https://blog.apollographql.com/apollo-router-v1-12-improved-router-security-performance-and-extensibility) in addition to the documentation links above. - -## Enabling Enterprise features - -To enable support for GraphOS Router Enterprise features: - -- Your organization must have a [GraphOS Enterprise plan](https://www.apollographql.com/pricing/). -- You must run GraphOS Router v1.12. [Download the latest version.](/router/quickstart#download-options) - - Certain Enterprise features might require a later router version. See a particular feature's documentation for details. -- Your router instances must connect to GraphOS with a **graph API key** and **graph ref** associated with your organization. - - You connect your router to GraphOS by setting [these environment variables](/router/configuration/overview/#environment-variables) when starting the router. - - If your router _already_ connects to your GraphOS Enterprise organization, no further action is required. - -After enabling support, you can begin using all Enterprise features. Consult [the documentation for each feature](#list-of-features) to learn more. - -### The Enterprise license - -Whenever your router instance starts up and connects to GraphOS, it fetches a **license**, which is the credential that authorizes its use of Enterprise features: - -```mermaid -flowchart LR; - subgraph "Your Infrastructure"; - router(["GraphOS Router"]); - end; - subgraph "GraphOS"; - uplink(Apollo Uplink) - end; - router--"Fetches supergraph schema
and license"-->uplink; -``` - -A router instance retains its license for the duration of its execution. If you terminate a router instance and then later start a new instance on the same machine, it must fetch a new license. - -Licenses are served via [Apollo Uplink](/federation/managed-federation/uplink/), the same multi-cloud endpoint that your router uses to fetch its supergraph schema from GraphOS. Because of this, licenses introduce no additional network dependencies, meaning your router's uptime remains unaffected. To learn more about multi-cloud Uplink, read the [Apollo blog post](https://www.apollographql.com/blog/announcement/backend/introducing-multi-cloud-support-for-apollo-uplink). - -A router instance's license is valid for the duration of your organization's current subscription billing period (plus a [grace period](#grace-period-for-expired-plans)), even if the router temporarily becomes disconnected from GraphOS. - - - -### Offline Enterprise license - - - - - -Offline Enterprise license support is available on an as-needed basis. Send a request to your Apollo contact to enable it for your GraphOS Studio organization. - - - -Running your GraphOS Router fleet while fully connected to GraphOS is the best choice for most Apollo users. However, some scenarios can prevent your routers from connecting to GraphOS for an extended period, ranging from disasters that break connectivity to isolated sites operating with air-gapped networks. If you need to restart or rapidly scale your entire router fleet, but you're unable to communicate with Apollo Uplink, new router instances won't be able to serve traffic. - -To support long-term disconnection scenarios, GraphOS supports **offline Enterprise licenses** for the GraphOS Router. An offline license enables routers to start and serve traffic without a persistent connection to GraphOS. The functionality available with an offline license is much like being fully connected to GraphOS, including managed federation for supergraph CI (operation checks, schema linting, contracts, etc.). - -An offline license can be retrieved from GraphOS with the [`rover license fetch`](/rover/commands/license) command. - -With an offline license, a router can either be fully disconnected from GraphOS or configured to connect to GraphOS on a best-effort basis so that it can send graph usage metrics. Apollo recommends configuring your router to report graph usage metrics to GraphOS whenever possible. Since your router sends metrics in a best-effort fashion, it incurs no performance or uptime penalties while enabling several powerful GraphOS features, such as operation checks, field insights, operation traces, and contracts. - - - -A router using an offline license requires [the use of local manifests](/router/configuration/persisted-queries/#experimental_local_manifests) when using [safelisting with persisted queries](/router/configuration/persisted-queries), otherwise it will not work as designed when the router is disconnected from Uplink. - - - -An offline license is valid for the lesser of the duration of your contract with Apollo, or one year, with an added grace period of 28 days. You are responsible for keeping your offline license files up to date within your infrastructure by rerunning `rover license fetch` to fetch updated license files. - -#### Set up offline license for the GraphOS Router - -Follow these steps to configure an GraphOS Router to use an offline Enterprise license: - -1. Fetch an offline license by running [`rover license fetch`](/rover/commands/license/#license-fetch) with the ID of the graph from which you want to fetch a license: - - ```bash - rover license fetch --graph-id - ``` - -1. Provide the offline license to your router on startup. The router accepts an offline license in a few ways: - - 1. [`--license `](/router/configuration/overview/#--license) CLI option, with an argument containing an absolute or relative path to an offline license file - 1. [`APOLLO_ROUTER_LICENSE_PATH`](/router/configuration/overview/#--license) environment variable, containing an absolute or relative path to an offline license file - 1. [`APOLLO_ROUTER_LICENSE`](/router/configuration/overview/#--license) environment variable, containing the stringified contents of an offline license file - - - - - The router checks the CLI option and environment variables in the listed order, then it uses the value of the first option or variable that is set. - - The `--license ` option is only available when running the router binary. When running a router with `rover dev`, you must use environment variables to provide your offline license. - - - -1. Configure the router to use a local supergraph schema by setting one of the following: - - * [`--s/-supergraph`](/router/configuration/overview/#-s----supergraph) CLI option, with an argument containing an absolute or relative path to supergraph schema file - * [`APOLLO_SUPERGRAPH_PATH`](/router/configuration/overview/#-s----supergraph) environment variable, containing an absolute or relative path to supergraph schema file - * [`APOLLO_SUPERGRAPH_URLS`](/router/configuration/overview/#-s----supergraph) environment variable, containing URLs to supergraph schemas - -1. (**Recommended**) Configure the router to report usage metrics to GraphOS in a best-effort basis by setting both the [`APOLLO_KEY`](/router/configuration/overview/#apollo_key) and [`APOLLO_GRAPH_REF`](/router/configuration/overview#apollo_graph_ref) environment variables. - - These metrics are necessary for several important GraphOS features (operations checks, field insights, operation traces, contracts). Sending them best-effort incurs no performance or uptime penalties. - -### Licenses with local development - -You might also need to run an GraphOS Router instance on your local machine, such as with the [`rover dev`](/graphos/graphs/local-development) command. It's likely that your local router instance doesn't connect to GraphOS to get its supergraph schema from Uplink. For example, you can run `rover dev` to perform composition locally. - -**You _can_ use Enterprise router features with a locally composed supergraph schema!** To do so, your router must still connect to GraphOS to obtain its [license](#the-enterprise-license). - -#### Set up local development - -These steps work both for running the router executable directly (`./router`) and for running it via `rover dev`: - -1. [Create a new variant](/graphos/graphs/federated-graphs/#adding-a-variant-via-the-rover-cli) for your supergraph that you'll use _only_ to fetch Enterprise licenses. - - Give the variant a name that clearly distinguishes it from variants that track schemas and metrics. - - Every team member that runs a router locally can use this same variant. - - When you create this variant, publish a dummy subgraph schema like the following (your router won't use it): - - ```graphql - type Query { - hello: String - } - ``` - -2. Create a [graph API key](/graphos/api-keys/#graph-api-keys) for your supergraph and assign it the **Contributor** role. - - We recommend creating a separate graph API key for _each team member_ that will run the router locally. - -3. When you start up your local router with your usual command, set the `APOLLO_GRAPH_REF` and `APOLLO_KEY` environment variables for that command: - - ```bash - APOLLO_GRAPH_REF="..." APOLLO_KEY="..." ./router --supergraph schema.graphql - ``` - - - The value of `APOLLO_GRAPH_REF` is the graph ref for the new, license-specific variant you created (e.g., `docs-example-graph@local-licenses`). - - The value of `APOLLO_KEY` is the graph API key you created. - -4. Your router will fetch an Enterprise license while using its locally composed supergraph schema. - -### Common errors - -**If your router doesn't successfully connect to GraphOS,** it logs an error that begins with one of the following strings if any Enterprise features are enabled: - -| Error Message | Description | -|-----------------------------|-------------| -| `Not connected to GraphOS.` | At least one of the `APOLLO_KEY` and `APOLLO_GRAPH_REF` environment variables wasn't set on router startup. | -| `License not found.` | The router connected to GraphOS with credentials that are not associated with a GraphOS Enterprise plan. | -| `License has expired.` | Your organization's GraphOS Enterprise subscription has ended. **Your router will stop processing incoming requests at the end of the standard [grace period](#grace-period-for-expired-plans).** | - -## Disabling Enterprise features - -To disable an Enterprise feature, remove all of its associated configuration keys from your router's [YAML config file](/router/configuration/overview/#yaml-config-file). - -## Grace period for expired plans - -If your organization terminates its GraphOS Enterprise subscription, your router's Enterprise license is considered expired at the end of your final paid subscription period. GraphOS provides a grace period for expired licenses so that you can disable Enterprise features before they produce breaking errors in your router. - -If your router has an expired Enterprise license, its behavior degrades according to the following schedule, _if_ any Enterprise features are still enabled: - -- **For the first 14 days after your license expires,** your router continues to behave as though it has a valid license. -- **After 14 days,** your router begins a **soft outage**: it continues processing client requests, but it emits logs and metrics that indicate it's experiencing an outage. -- **After 28 days,** your router begins a **hard outage**. It no longer processes incoming client requests and continues emitting logs and metrics from the soft outage. - -Your router resumes normal functioning whenever you renew your GraphOS Enterprise subscription or disable all [Enterprise features](#list-of-features). diff --git a/docs/source/reference/router/configuration.mdx b/docs/source/reference/router/configuration.mdx index 3c89326a73..144fc63edf 100644 --- a/docs/source/reference/router/configuration.mdx +++ b/docs/source/reference/router/configuration.mdx @@ -1114,6 +1114,8 @@ The router rejects any request that violates at least one of these limits. limits: # Network-based limits http_max_request_bytes: 2000000 # Default value: 2 MB + http1_max_request_headers: 200 # Default value: 100 + http1_max_request_buf_size: 800kib # Default value: 400kib # Parser-based limits parser_max_tokens: 15000 # Default value @@ -1145,6 +1147,24 @@ Before increasing this limit significantly consider testing performance in an environment similar to your production, especially if some clients are untrusted. Many concurrent large requests could cause the router to run out of memory. +##### `http1_max_request_headers` + +Limit the maximum number of headers of incoming HTTP1 requests. +The default value is 100 headers. + +If router receives more headers than the buffer size, it responds to the client with `431 Request Header Fields Too Large`. + +##### `http1_max_request_buf_size` + +Limit the maximum buffer size for the HTTP1 connection. Default is ~400kib. + +Note for Rust Crate Users: If you are using the Router as a Rust crate, the `http1_request_max_buf_size` option requires the `hyper_header_limits` feature and also necessitates using Apollo's fork of the Hyper crate until the [changes are merged upstream](https://github.com/hyperium/hyper/pull/3523). +You can include this fork by adding the following patch to your Cargo.toml file: +```toml +[patch.crates-io] +"hyper" = { git = "https://github.com/apollographql/hyper.git", tag = "header-customizations-20241108" } +``` + #### Parser-based limits ##### `parser_max_tokens` @@ -1236,21 +1256,34 @@ example: password: "${env.MY_PASSWORD}" #highlight-line ``` -### Fragment reuse and generation + +
+### Fragment generation and reuse -By default, the router will attempt to reuse fragments from the original query while forming subgraph requests. This behavior can be disabled by setting the option to `false`: +By default, the router compresses subgraph requests by generating fragment +definitions based on the shape of the subgraph operation. In many cases this +significantly reduces the size of the query sent to subgraphs. + +The router also supports an experimental algorithm that attempts to reuse fragments +from the original operation while forming subgraph requests. This experimental feature +used to be enabled by default, but is still available to support subgraphs that rely +on the specific shape of fragments in an operation: ```yaml supergraph: - experimental_reuse_query_fragments: false + generate_query_fragments: false + experimental_reuse_query_fragments: true ``` -Alternatively, the router can be configured to _generate_ fragments for subgraph requests. When set to `true`, the router will extract _inline fragments only_ into fragment definitions before sending queries to subgraphs. This can significantly reduce the size of the query sent to subgraphs, but may increase the time it takes for planning. Note that this option and `experimental_reuse_query_fragments` are mutually exclusive; if both are explicitly set to `true`, `generate_query_fragments` will take precedence. +Note that `generate_query_fragments` and `experimental_reuse_query_fragments` are +mutually exclusive; if both are explicitly set to `true`, `generate_query_fragments` +will take precedence. -```yaml -supergraph: - generate_query_fragments: true -``` + + +In the future, the `generate_query_fragments` option will be the only option for handling fragments. + + diff --git a/docs/source/reference/router/errors.mdx b/docs/source/reference/router/errors.mdx index f325df83fb..aff203ecf4 100644 --- a/docs/source/reference/router/errors.mdx +++ b/docs/source/reference/router/errors.mdx @@ -93,7 +93,7 @@ The actual cost of the query was greater than the configured maximum cost. The query could not be parsed. - + The response from a subgraph did not match the GraphQL schema. diff --git a/docs/source/reference/router/rhai.mdx b/docs/source/reference/router/rhai.mdx index 4a74cc28f9..23b11026ec 100644 --- a/docs/source/reference/router/rhai.mdx +++ b/docs/source/reference/router/rhai.mdx @@ -405,6 +405,8 @@ request.subgraph.uri.path request.subgraph.uri.port ``` +**For `subgraph_service` callbacks only,** the `request` object provides the non modifiable field `request.subgraph_request_id` which is a unique ID corresponding to the subgraph request. + ### `request.context` The context is a generic key/value store that exists for the entire lifespan of a particular client request. You can use this to share information between multiple callbacks throughout the request's lifespan. @@ -594,6 +596,8 @@ response.body.extensions All of the above fields are read/write. +**For `subgraph_service` callbacks only,** the `response` object provides the non modifiable field `response.subgraph_request_id` which is a unique ID corresponding to the subgraph request, and the same id that can be obtained on the request side. + The following fields are identical in behavior to their `request` counterparts: * [`context`](#requestcontext) diff --git a/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx b/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx index bf0101d802..da9b655036 100644 --- a/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx +++ b/docs/source/reference/router/telemetry/instrumentation/standard-instruments.mdx @@ -65,11 +65,15 @@ The coprocessor operations metric has the following attributes: - `apollo_router.query_planning.warmup.duration` - Time spent warming up the query planner queries in seconds. - `apollo.router.query_planning.plan.duration` - Histogram of plan durations isolated to query planning time only. - `apollo.router.query_planning.total.duration` - Histogram of plan durations including queue time. -- `apollo.router.query_planning.queued` - A gauge of the number of queued plans requests. +- `apollo.router.query_planning.queued` - When the legacy planner is used, a gauge of the number of queued plans requests. - `apollo.router.query_planning.plan.evaluated_plans` - Histogram of the number of evaluated query plans. - `apollo.router.v8.heap.used` - heap memory used by V8, in bytes. - `apollo.router.v8.heap.total` - total heap allocated by V8, in bytes. +### Compute jobs + +- `apollo.router.compute_jobs.queued` - A gauge of the number of jobs queued for the thread pool dedicated to CPU-heavy components like GraphQL parsing and validation, and the (new) query planner. + ### Uplink diff --git a/docs/source/routing/customization/coprocessor.mdx b/docs/source/routing/customization/coprocessor.mdx index 9d9f74ad14..314182b008 100644 --- a/docs/source/routing/customization/coprocessor.mdx +++ b/docs/source/routing/customization/coprocessor.mdx @@ -504,6 +504,7 @@ Properties of the JSON body are divided into two high-level categories: "stage": "SubgraphRequest", "control": "continue", "id": "666d677225c1bc6d7c54a52b409dbd4e", + "subgraphRequestId": "b5964998b2394b64a864ef802fb5a4b3", // Data properties "headers": {}, @@ -580,6 +581,7 @@ Properties of the JSON body are divided into two high-level categories: "version": 1, "stage": "SubgraphResponse", "id": "b7810c6f7f95640fd6c6c8781e3953c0", + "subgraphRequestId": "b5964998b2394b64a864ef802fb5a4b3", "control": "continue", // Data properties @@ -758,6 +760,23 @@ A unique ID corresponding to the client request associated with this coprocessor +##### `subgraphRequestId` + +`string` + + + + +A unique ID corresponding to the subgraph request associated with this coprocessor request (only available at the `SubgraphRequest` and `SubgraphResponse` stages). + +**Do not return a _different_ value for this property.** If you do, the router treats the coprocessor request as if it failed. + + + + + + + ##### `stage` `string` diff --git a/docs/source/routing/customization/overview.mdx b/docs/source/routing/customization/overview.mdx index 4eee05923e..c417b06d19 100644 --- a/docs/source/routing/customization/overview.mdx +++ b/docs/source/routing/customization/overview.mdx @@ -209,7 +209,7 @@ httpServer --"14. HTTP response" --> client For simplicity's sake, the preceding diagrams show the request and response sides separately and sequentially. In reality, some requests and responses may happen simultaneously and repeatedly. -For example, `SubgraphRequest`s can happen both in parallel _and_ in sequence: one subgraph's response may be necessary for another's `SubgraphRequest`. (The query planner decides which requests can happen in parallel vs. which need to happen in sequence.) +For example, `SubgraphRequest`s can happen both in parallel _and_ in sequence: one subgraph's response may be necessary for another's `SubgraphRequest`. (The query planner decides which requests can happen in parallel vs. which need to happen in sequence). To match subgraph requests to responses in customizations, the router exposes a `subgraph_request_id` field that will hold the same value in paired requests and responses. ##### Requests run in parallel diff --git a/docs/source/routing/observability/client-id-enforcement.mdx b/docs/source/routing/observability/client-id-enforcement.mdx index 76e2766606..474405547e 100644 --- a/docs/source/routing/observability/client-id-enforcement.mdx +++ b/docs/source/routing/observability/client-id-enforcement.mdx @@ -64,6 +64,13 @@ fn process_request(request) { See a runnable example Rhai script in the [Apollo Solutions repository](https://github.com/apollosolutions/example-rhai-client-id-validation). + + +If you're an enterprise customer looking for more material on this topic, try the [Enterprise best practices: Router extensibility](https://www.apollographql.com/tutorials/router-extensibility) course on Odyssey. + +Not an enterprise customer? [Learn about GraphOS for Enterprise.](https://www.apollographql.com/pricing) + + ## Enforcing in Apollo Server diff --git a/docs/source/routing/observability/subgraph-error-inclusion.mdx b/docs/source/routing/observability/subgraph-error-inclusion.mdx index d991a3e710..ee28a0672d 100644 --- a/docs/source/routing/observability/subgraph-error-inclusion.mdx +++ b/docs/source/routing/observability/subgraph-error-inclusion.mdx @@ -31,3 +31,21 @@ To report the subgraph errors to GraphOS that is a separate configuration that i ## Logging GraphQL request errors To log the GraphQL error responses (i.e. messages returned in the GraphQL `errors` array) from the router, see the [logging configuration documentation](/router/configuration/telemetry/exporters/logging/overview). +## Exposing subgraph name through error extensions +If `include_subgraph_errors` is `true` for a particular subgraph, all errors originating in this subgraph will have the subgraph's name exposed as a `service` extension. + +For example, if subgraph errors are enabled for the `products` subgraph and this subgraph returns an error, it will have a `service` extension: +```json +{ + "data": null, + "errors": [ + { + "message": "Invalid product ID", + "path": [], + "extensions": { + "service": "products", + } + } + ] +} +``` diff --git a/docs/source/routing/performance/caching/entity.mdx b/docs/source/routing/performance/caching/entity.mdx index 218e6bd12a..64618ca2d5 100644 --- a/docs/source/routing/performance/caching/entity.mdx +++ b/docs/source/routing/performance/caching/entity.mdx @@ -61,6 +61,83 @@ With entity caching enabled for this example, the router can: - Cache the cart per user, with a small amount of data. - Cache inventory data with a short TTL or not cache it at all. +For example, the diagram below shows how a price entity can be cached and then combined with purchase and inventory fragments to serve a `products` query. Because price data is subject to change less often than inventory data, it makes sense to cache it with a different TTL. + +```mermaid +flowchart RL + subgraph QueryResponse["JSON Response"] + n1["{ +   "products": [ +     { +       "name": "DraftSnowboard", +       "pastPurchases": ..., +       "price": { +         "amount": "1500", +         "currency_code": "USD" +       }, +       "quantity": "250" +     }, +     ... +   ] + }"] + end + + subgraph Subgraphs["Subgraphs"] + Purchases["Purchases"] + Inventory["Inventory"] + Price["Price"] + end + + subgraph PriceQueryFragment["Price Query Fragment (e.g. TTL 2200)"] + n2["{ +   "price": { +     "id": 101, +     "product_id": 12, +     "amount": 1500, +     "currency_code": "USD" +   } + }"] + end + + subgraph PurchaseHistoryQueryFragment["Purchase History Query Fragment"] + n3["{ +   "purchases": { +     "product_id": 12, +     "user_id": 2932, +     ... +   } + }"] + end + + subgraph InventoryQueryFragment["Inventory Query Fragment"] + n4["{ +   "inventory": { +     "id": 19, +     "product_id": 12, +     "quantity": 250 +   } + }"] + end + + Router + Database[("   ")] + + Router --> QueryResponse + Purchases --> Router + Inventory --> Router + Price --- Router + PriceQueryFragment --> Database + PurchaseHistoryQueryFragment --> Purchases + InventoryQueryFragment --> Inventory + Database --> Router + + style n1 text-align:left + style n2 text-align:left + style n3 text-align:left + style n4 text-align:left + style Price border:none,stroke-width:1px,stroke-dasharray:5 5,stroke:#A6A6A6 +``` + ## Use entity caching Follow this guide to enable and configure entity caching in the GraphOS Router. @@ -85,6 +162,7 @@ For example: # Enable entity caching globally preview_entity_cache: enabled: true + expose_keys_in_context: true # Optional, it will expose cache keys in the context in order to use it in coprocessors or Rhai subgraph: all: enabled: true @@ -135,6 +213,56 @@ This entry contains an object with the `all` field to affect all subgraph reques ### Entity cache invalidation +You can invalidate entity cache entries with a [specifically formatted request](#invalidation-request-format once you [configure your router](#configuration) appropriately. For example, if price data changes before a price entity's TTL expires, you can send an invalidation request. + +```mermaid + +flowchart RL + subgraph QueryResponse["Cache invalidation POST"] + n1["{ +   "kind": "subgraph", +   "subgraph": "price", +   "type": "Price", +   "key": { +     "id": "101" +   } + }"] + end + + subgraph Subgraphs["Subgraphs"] + Purchases["Purchases"] + Inventory["Inventory"] + Price["Price"] + end + + subgraph PriceQueryFragment["Price Query Fragment (e.g. TTL 2200)"] + n2[" ̶{̶ +   " ̶p̶r̶i̶c̶e̶": ̶{̶ +     " ̶i̶d̶": ̶1̶0̶1̶, +     " ̶p̶r̶o̶d̶u̶c̶t̶_̶i̶d̶": ̶1̶2̶, +     " ̶a̶m̶o̶u̶n̶t̶": ̶1̶5̶0̶0̶, +     "̶c̶u̶r̶r̶e̶n̶c̶y̶_̶c̶o̶d̶e̶": " ̶U̶S̶D̶" +    ̶}̶ + ̶}̶"] + end + + Router + Database[("   ")] + + QueryResponse --> Router + Purchases --> Router + Inventory --> Router + Price --- Router + PriceQueryFragment --> Database + Database --> Router + + style n1 text-align:left + style Price border:none,stroke-width:1px,stroke-dasharray:5 5,stroke:#A6A6A6 + style Purchases border:none,stroke-width:1px,stroke-dasharray:5 5,stroke:#A6A6A6 + style Inventory border:none,stroke-width:1px,stroke-dasharray:5 5,stroke:#A6A6A6 + style n2 text-align:left +``` + When existing cache entries need to be replaced, the router supports a couple of ways for you to invalidate entity cache entries: - [**Invalidation endpoint**](#invalidation-http-endpoint) - the router exposes an invalidation endpoint that can receive invalidation requests from any authorized service. This is primarily intended as an alternative to the extensions mechanism described below. For example a subgraph could use it to trigger invalidation events "out of band" from any requests received by the router or a platform operator could use it to invalidate cache entries in response to events which aren't directly related to a router. - **Subgraph response extensions** - you can send invalidation requests via subgraph response extensions, allowing a subgraph to invalidate cached data right after a mutation. diff --git a/docs/source/routing/security/authorization.mdx b/docs/source/routing/security/authorization.mdx index f0a07eab2a..94a6ed356c 100644 --- a/docs/source/routing/security/authorization.mdx +++ b/docs/source/routing/security/authorization.mdx @@ -688,11 +688,12 @@ When using subscriptions along with `@policy` authorization, subscription events ## Composition and federation -GraphOS's composition strategy for authorization directives is intentionally accumulative. When you define authorization directives on fields and types in subgraphs, GraphOS composes them into the supergraph schema. In other words, if subgraph fields or types include `@requiresScopes`, `@authenticated`, or `@policy` directives, they are set on the supergraph too. +GraphOS's composition strategy for authorization directives is intentionally accumulative. When you define authorization directives on fields and types in subgraphs, GraphOS composes them into the supergraph schema. In other words, if subgraph fields or types include `@requiresScopes`, `@authenticated`, or `@policy` directives, they are set on the supergraph too. Whether composition uses `AND` or `OR` logic depends on how the authorization directives are used. -#### Composition with `AND`/`OR` logic +### Composed fields with different authorization directives -If shared subgraph fields include multiple directives, composition merges them. For example, suppose the `me` query requires `@authentication` in one subgraph: +If a shared field uses different authorization directives across subgraphs, composition merges them using `AND` logic. +For example, suppose the `me` query requires `@authenticated` in one subgraph and the `read:user` scope in another subgraph: ```graphql title="Subgraph A" @@ -707,8 +708,6 @@ type User { } ``` -and the `read:user` scope in another subgraph: - ```graphql title="Subgraph B" type Query { me: User @requiresScopes(scopes: [["read:user"]]) @@ -721,9 +720,19 @@ type User { } ``` -A request would need to both be authenticated **AND** have the required scope. Recall that the `@authenticated` directive only checks for the existence of the `apollo_authentication::JWT::claims` key in a request's context, so authentication is guaranteed if the request includes scopes. +A request must both be authenticated **AND** have the required `read:user` scope to succeed. + + + +Recall that the `@authenticated` directive only checks for the existence of the `apollo_authentication::JWT::claims` key in a request's context, so authentication is guaranteed if the request includes scopes. -If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges them with the same logic used to [combine scopes for a single use of `@requiresScopes`](#combining-required-scopes-with-andor-logic). For example, if one subgraph requires the `read:others` scope on the `users` query: + + +### Composed fields with the same authorization directives + +If a shared field uses the same authorization directives across subgraphs, composition merges them using `OR` logic. +For example, suppose two subgraphs use the `@requiresScopes` directive on the `users` query. +One subgraph requires the `read:others` scope, and another subgraph requires the `read:profiles` scope: ```graphql title="Subgraph A" type Query { @@ -731,23 +740,32 @@ type Query { } ``` -and another subgraph requires the `read:profiles` scope on `users` query: - ```graphql title="Subgraph B" type Query { users: [User!]! @requiresScopes(scopes: [["read:profiles"]]) } ``` -Then the supergraph schema would require _both_ scopes for it. +A request would need either the `read:others` **OR** the `read:profiles` scope to be authorized. ```graphql title="Supergraph" type Query { - users: [User!]! @requiresScopes(scopes: [["read:others", "read:profiles"]]) + users: [User!]! @requiresScopes(scopes: [["read:others"], ["read:profiles"]]) } ``` -As with [combining scopes for a single use of `@requiresScopes`](#combining-required-scopes-with-andor-logic), you can use nested arrays to introduce **OR** logic: + + +Refer to the section on [Combining policies with AND/OR logic](#combining-policies-with-andor-logic) for a refresher of `@requiresScopes` boolean syntax. + + + +Using **OR** logic for shared directives simplifies schema updates. +If requirements change suddenly, you don't need to update the directive in all subgraphs simultaneously. + + +#### Combining `AND`/`OR` logic with `@requiresScopes` +As with [combining scopes for a single use of [`@requiresScopes`](#combining-required-scopes-with-andor-logic), you can use nested arrays to introduce **AND** logic in a single subgraph: ```graphql title="Subgraph A" type Query { @@ -761,7 +779,7 @@ type Query { } ``` -Since both `scopes` arrays are nested arrays, they would be composed using **OR** logic into the supergraph schema: +Since both subgraphs use the same authorization directive, composition [merges them using **OR** logic](#a-shared-field-with-the-same-authorization-directives-use-or-logic): ```graphql title="Supergraph" type Query { @@ -773,8 +791,8 @@ This syntax means a request needs either (`read:others` **AND** `read:users`) sc ### Authorization and `@key` fields -The [`@key` directive](https://www.apollographql.com/docs/federation/entities/) lets you create an entity whose fields resolve across multiple subgraphs. -If you use authorization directives on fields defined in [`@key` directives](https://www.apollographql.com/docs/federation/entities/), Apollo still uses those fields to compose entities between the subgraphs, but the client cannot query them directly. +The [`@key` directive](/graphos/reference/federation/directives#key) lets you create an entity whose fields resolve across multiple subgraphs. +If you use authorization directives on fields defined in `@key` directives, Apollo still uses those fields to compose entities between the subgraphs, but the client cannot query them directly. Consider these example subgraph schemas: @@ -825,11 +843,11 @@ query { } ``` -This behavior resembles what you can create with [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). +This behavior resembles what you can create with [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](/graphos/reference/federation/directives#inaccessible). ### Authorization and interfaces -If a type [implementing an interface](https://www.apollographql.com/docs/apollo-server/schema/unions-interfaces/#interface-type) requires authorization, unauthorized requests can query the interface, but not any parts of the type that require authorization. +If a type [implementing an interface](/apollo-server/schema/unions-interfaces/#interface-type) requires authorization, unauthorized requests can query the interface, but not any parts of the type that require authorization. For example, consider this schema where the `Post` interface doesn't require authentication, but the `PrivateBlog` type, which implements `Post`, does: diff --git a/docs/source/routing/security/request-limits.mdx b/docs/source/routing/security/request-limits.mdx index 3cbe0ff552..3feb97f84f 100644 --- a/docs/source/routing/security/request-limits.mdx +++ b/docs/source/routing/security/request-limits.mdx @@ -15,6 +15,8 @@ For enhanced security, the GraphOS Router can reject requests that violate any o limits: # Network-based limits http_max_request_bytes: 2000000 # Default value: 2 MB + http1_max_request_headers: 200 # Default value: 100 + http1_max_request_buf_size: 800kb # Default value: 400kib # Parser-based limits parser_max_tokens: 15000 # Default value @@ -273,6 +275,24 @@ Before increasing this limit significantly consider testing performance in an environment similar to your production, especially if some clients are untrusted. Many concurrent large requests could cause the router to run out of memory. +### `http1_max_request_headers` + +Limit the maximum number of headers of incoming HTTP1 requests. +The default value is 100 headers. + +If router receives more headers than the buffer size, it responds to the client with `431 Request Header Fields Too Large`. + +### `http1_max_request_buf_size` + +Limit the maximum buffer size for the HTTP1 connection. Default is ~400kib. + +Note for Rust Crate Users: If you are using the Router as a Rust crate, the `http1_request_max_buf_size` option requires the `hyper_header_limits` feature and also necessitates using Apollo's fork of the Hyper crate until the [changes are merged upstream](https://github.com/hyperium/hyper/pull/3523). +You can include this fork by adding the following patch to your Cargo.toml file: +```toml +[patch.crates-io] +"hyper" = { git = "https://github.com/apollographql/hyper.git", tag = "header-customizations-20241108" } +``` + ## Parser-based limits ### `parser_max_tokens` diff --git a/docs/source/routing/self-hosted/containerization/index.mdx b/docs/source/routing/self-hosted/containerization/index.mdx index 2134e6a6b8..9a66625c11 100644 --- a/docs/source/routing/self-hosted/containerization/index.mdx +++ b/docs/source/routing/self-hosted/containerization/index.mdx @@ -32,4 +32,4 @@ For examples of customizing and deploying router images in specific environments See the Apollo Solutions [example Cloud Foundry deployment](https://github.com/apollosolutions/example-pcf-deployment) for a minimal router configuration and Cloud Foundry manifest file. - \ No newline at end of file + diff --git a/examples/coprocessor-surrogate-cache-key/README.md b/examples/coprocessor-surrogate-cache-key/README.md new file mode 100644 index 0000000000..059c6466f5 --- /dev/null +++ b/examples/coprocessor-surrogate-cache-key/README.md @@ -0,0 +1,124 @@ +## Context + +Existing caching systems often support a concept of surrogate keys, where a key can be linked to a specific piece of cached data, independently of the actual cache key. + +As an example, a news website might want to invalidate all cached articles linked to a specific company or person following an event. To that end, when returning the article, the service can add a surrogate key to the article response, and the cache would keep a map from surrogate keys to cache keys. + +## Surrogate keys and the router’s entity cache + +To support a surrogate key system with the entity caching in the router, we make the following assumptions: + +- The subgraph returns surrogate keys with the response. The router will not manipulate those surrogate keys directly. Instead, it leaves that task to a coprocessor +- The coprocessor tasked with managing surrogate keys will store the mapping from surrogate keys to cache keys. It will be useful to invalidate all cache keys related to a surrogate cache key in Redis. +- The router will expose a way to gather the cache keys used in a subgraph request + +### Router side support + +The router has two features to support surrogate cache key: + +- An id field for subgraph requests and responses. This is a random, unique id per subgraph call that can be used to keep state between the request and response side, and keep data from the various subgraph calls separately for the entire client request. You have to enable it in configuration (`subgraph_request_id`): + +```yaml title=router.yaml +coprocessor: + url: http://127.0.0.1:3000 # mandatory URL which is the address of the coprocessor + supergraph: + response: + context: true + subgraph: + all: + response: + subgraph_request_id: true + context: true +``` + +- The entity cache has an option to store in the request context, at the key `apollo::entity_cache::cached_keys_status`, a map `subgraph request id => cache keys` only when it's enabled in the configuration (`expose_keys_in_context`)): + +```yaml title=router.yaml +preview_entity_cache: + enabled: true + expose_keys_in_context: true + metrics: + enabled: true + invalidation: + listen: 0.0.0.0:4000 + path: /invalidation + # Configure entity caching per subgraph + subgraph: + all: + enabled: true + # Configure Redis + redis: + urls: ["redis://localhost:6379"] + ttl: 24h # Optional, by default no expiration +``` + +The coprocessor will then work at two stages: + +- Subgraph response: + - Extract the subgraph request id + - Extract the list of surrogate keys from the response +- Supergraph stage: + - Extract the map `subgraph request id => cache keys` + - Match it with the surrogate cache keys obtained at the subgraph response stage + +The coprocessor then has a map of `surrogate keys => cache keys` that it can use to invalidate cached data directly from Redis. + +### Example workflow + +- The router receives a client request +- The router starts a subgraph request: + - The entity cache plugin checks if the request has a corresponding cached entry: + - If the entire response can be obtained from cache, we return a response here + - If it cannot be obtained, or only partially (\_entities query), a request is transmitted to the subgraph + - The subgraph responds to the request. The response can contain a list of surrogate keys in a header: `Surrogate-Keys: homepage, feed` + - The subgraph response stage coprocessor extracts the surrogate keys from headers, and stores it in the request context, associated with the subgraph request id `0e67db40-e98d-4ad7-bb60-2012fb5db504`: + +```json +{ + "​0ee3bf47-5e8d-47e3-8e7e-b05ae877d9c7": ["homepage", "feed"] +} +``` + +- The entity cache processes the subgraph response: + - It generates a new subgraph response by interspersing data it got from cache with data from the original response + - It stores the list of keys in the context. `new` indicates newly cached data coming from the subgraph, linked to the surrogate keys, while `cached` is data obtained from the cache. These are the keys directly used in Redis: + +```json +{ + "apollo::entity_cache::cached_keys_status": { + "0ee3bf47-5e8d-47e3-8e7e-b05ae877d9c7": [ + { + "key": "version:1.0:subgraph:products:type:Query:hash:af9febfacdc8244afc233a857e3c4b85a749355707763dc523a6d9e8964e9c8d:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "status": "new", + "cache_control": "max-age=60,public" + } + ] + } +} +``` + +- The supergraph response stage loads data from the context and creates the mapping: + +```json +{ + "homepage": [ + { + "key": "version:1.0:subgraph:products:type:Query:hash:af9febfacdc8244afc233a857e3c4b85a749355707763dc523a6d9e8964e9c8d:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "status": "new", + "cache_control": "max-age=60,public" + } + ], + "feed": [ + { + "key": "version:1.0:subgraph:products:type:Query:hash:af9febfacdc8244afc233a857e3c4b85a749355707763dc523a6d9e8964e9c8d:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "status": "new", + "cache_control": "max-age=60,public" + } + ] +} +``` + +- When a surrogate key must be used to invalidate data, that mapping is used to obtained the related cache keys + + +In this example we provide a very simple implementation using in memory data in NodeJs. It just prints the mapping at the supergraph response level to show you how you can create that mapping. diff --git a/examples/coprocessor-surrogate-cache-key/nodejs/.gitignore b/examples/coprocessor-surrogate-cache-key/nodejs/.gitignore new file mode 100644 index 0000000000..d5f19d89b3 --- /dev/null +++ b/examples/coprocessor-surrogate-cache-key/nodejs/.gitignore @@ -0,0 +1,2 @@ +node_modules +package-lock.json diff --git a/examples/coprocessor-surrogate-cache-key/nodejs/README.md b/examples/coprocessor-surrogate-cache-key/nodejs/README.md new file mode 100644 index 0000000000..e65978e36b --- /dev/null +++ b/examples/coprocessor-surrogate-cache-key/nodejs/README.md @@ -0,0 +1,16 @@ +# External Subgraph nodejs example + +This is an example that involves a nodejs coprocessor alongside a router. + +## Usage + +- Start the coprocessor: + +```bash +$ npm ci && npm run start +``` + +- Start the router +``` +$ APOLLO_KEY="YOUR_APOLLO_KEY" APOLLO_GRAPH_REF="YOUR_APOLLO_GRAPH_REF" cargo run -- --configuration router.yaml +``` diff --git a/examples/coprocessor-surrogate-cache-key/nodejs/package.json b/examples/coprocessor-surrogate-cache-key/nodejs/package.json new file mode 100644 index 0000000000..1c3ef66300 --- /dev/null +++ b/examples/coprocessor-surrogate-cache-key/nodejs/package.json @@ -0,0 +1,15 @@ +{ + "name": "coprocessor", + "version": "1.0.0", + "description": "A coprocessor example for the router", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.18.2" + } +} \ No newline at end of file diff --git a/examples/coprocessor-surrogate-cache-key/nodejs/router.yaml b/examples/coprocessor-surrogate-cache-key/nodejs/router.yaml new file mode 100644 index 0000000000..d3b36e3966 --- /dev/null +++ b/examples/coprocessor-surrogate-cache-key/nodejs/router.yaml @@ -0,0 +1,36 @@ +supergraph: + listen: 127.0.0.1:4000 + introspection: true +sandbox: + enabled: true +homepage: + enabled: false +include_subgraph_errors: + all: true # Propagate errors from all subraphs + +coprocessor: + url: http://127.0.0.1:3000 # mandatory URL which is the address of the coprocessor + supergraph: + response: + context: true + subgraph: + all: + response: + subgraph_request_id: true + context: true +preview_entity_cache: + enabled: true + expose_keys_in_context: true + metrics: + enabled: true + invalidation: + listen: 0.0.0.0:4000 + path: /invalidation + # Configure entity caching per subgraph + subgraph: + all: + enabled: true + # Configure Redis + redis: + urls: ["redis://localhost:6379"] + ttl: 24h # Optional, by default no expiration \ No newline at end of file diff --git a/examples/coprocessor-surrogate-cache-key/nodejs/src/index.js b/examples/coprocessor-surrogate-cache-key/nodejs/src/index.js new file mode 100644 index 0000000000..daadb50740 --- /dev/null +++ b/examples/coprocessor-surrogate-cache-key/nodejs/src/index.js @@ -0,0 +1,110 @@ +const express = require("express"); +const app = express(); +const port = 3000; + +app.use(express.json()); + +// This is for demo purpose and will keep growing over the time +// It saves the value of surrogate cache keys returned by a subgraph request +let surrogateKeys = new Map(); +// Example: +// { +// "​​0e67db40-e98d-4ad7-bb60-2012fb5db504": [ +// "elections", +// "sp500" +// ], +// "​​0d77db40-e98d-4ad7-bb60-2012fb5db555": [ +// "homepage" +// ] +// } +// -------------- +// For every surrogate cache key we know the related cache keys +// Example: +// { +// "elections": [ +// "version:1.0:subgraph:reviews:type:Product:entity:4e48855987eae27208b466b941ecda5fb9b88abc03301afef6e4099a981889e9:hash:1de543dab57fde0f00247922ccc4f76d4c916ae26a89dd83cd1a62300d0cda20:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c" +// ], +// "sp500": [ +// "version:1.0:subgraph:reviews:type:Product:entity:4e48855987eae27208b466b941ecda5fb9b88abc03301afef6e4099a981889e9:hash:1de543dab57fde0f00247922ccc4f76d4c916ae26a89dd83cd1a62300d0cda20:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c" +// ] +// } + +app.post("/", (req, res) => { + const request = req.body; + console.log("✉️ Got payload:"); + console.log(JSON.stringify(request, null, 2)); + switch (request.stage) { + case "SubgraphResponse": + request.headers["surrogate-keys"] = ["homepage, feed"]; // To simulate + // Fetch the surrogate keys returned by the subgraph to create a mapping between subgraph request id and surrogate keys, to create the final mapping later + // Example: + // { + // "​​0e67db40-e98d-4ad7-bb60-2012fb5db504": [ + // "elections", + // "sp500" + // ] + // } + if (request.headers["surrogate-keys"] && request.subgraphRequestId) { + let keys = request.headers["surrogate-keys"] + .join(",") + .split(",") + .map((k) => k.trim()); + + surrogateKeys.set(request.subgraphRequestId, keys); + console.log("surrogateKeys", surrogateKeys); + } + break; + case "SupergraphResponse": + if ( + request.context && + request.context.entries && + request.context.entries["apollo::entity_cache::cached_keys_status"] + ) { + let contextEntry = + request.context.entries["apollo::entity_cache::cached_keys_status"]; + let mapping = {}; + Object.keys(contextEntry).forEach((request_id) => { + let cache_keys = contextEntry[`${request_id}`]; + let surrogateCachekeys = surrogateKeys.get(request_id); + if (surrogateCachekeys) { + // Create the mapping between surrogate cache keys and effective cache keys + // Example: + // { + // "elections": [ + // "version:1.0:subgraph:reviews:type:Product:entity:4e48855987eae27208b466b941ecda5fb9b88abc03301afef6e4099a981889e9:hash:1de543dab57fde0f00247922ccc4f76d4c916ae26a89dd83cd1a62300d0cda20:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c" + // ], + // "sp500": [ + // "version:1.0:subgraph:reviews:type:Product:entity:4e48855987eae27208b466b941ecda5fb9b88abc03301afef6e4099a981889e9:hash:1de543dab57fde0f00247922ccc4f76d4c916ae26a89dd83cd1a62300d0cda20:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c" + // ] + // } + + surrogateCachekeys.reduce((acc, current) => { + if (acc[`${current}`]) { + acc[`${current}`] = acc[`${current}`].concat(cache_keys); + } else { + acc[`${current}`] = cache_keys; + } + + return acc; + }, mapping); + } + }); + + console.log(mapping); + } + break; + default: + return res.json(request); + } + res.json(request); +}); + +app.listen(port, () => { + console.log(`🚀 Coprocessor running on port ${port}`); + console.log( + `Run a router with the provided router.yaml configuration to test the example:` + ); + console.log( + `APOLLO_KEY="YOUR_APOLLO_KEY" APOLLO_GRAPH_REF="YOUR_APOLLO_GRAPH_REF" cargo run -- --configuration router.yaml` + ); +}); diff --git a/helm/chart/router/Chart.yaml b/helm/chart/router/Chart.yaml index 9d7c28d3d0..51cf64fa63 100644 --- a/helm/chart/router/Chart.yaml +++ b/helm/chart/router/Chart.yaml @@ -20,10 +20,10 @@ type: application # so it matches the shape of our release process and release automation. # By proxy of that decision, this version uses SemVer 2.0.0, though the prefix # of "v" is not included. -version: 1.57.1 +version: 1.58.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.57.1" +appVersion: "v1.58.0" diff --git a/helm/chart/router/README.md b/helm/chart/router/README.md index 6dd2aaea77..c68ab5cd8b 100644 --- a/helm/chart/router/README.md +++ b/helm/chart/router/README.md @@ -2,7 +2,7 @@ [router](https://github.com/apollographql/router) Rust Graph Routing runtime for Apollo Federation -![Version: 1.57.1](https://img.shields.io/badge/Version-1.57.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.57.1](https://img.shields.io/badge/AppVersion-v1.57.1-informational?style=flat-square) +![Version: 1.58.0](https://img.shields.io/badge/Version-1.58.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.58.0](https://img.shields.io/badge/AppVersion-v1.58.0-informational?style=flat-square) ## Prerequisites @@ -11,7 +11,7 @@ ## Get Repo Info ```console -helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.57.1 +helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.58.0 ``` ## Install Chart @@ -19,7 +19,7 @@ helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.57.1 **Important:** only helm3 is supported ```console -helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.57.1 --values my-values.yaml +helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.58.0 --values my-values.yaml ``` _See [configuration](#configuration) below._ diff --git a/helm/chart/router/templates/virtualservice.yaml b/helm/chart/router/templates/virtualservice.yaml index 1112750a3c..3d273583d7 100644 --- a/helm/chart/router/templates/virtualservice.yaml +++ b/helm/chart/router/templates/virtualservice.yaml @@ -20,7 +20,13 @@ metadata: {{- end }} spec: hosts: + { { if .Values.virtualservice.Hosts } } + { { - range .Values.virtualservice.Hosts } } + - { { . | quote } } + { { - end } } + { { - else } } - "*" + { { - end } } {{- if .Values.virtualservice.gatewayName }} gateways: - {{ .Values.virtualservice.gatewayName }} diff --git a/helm/chart/router/values.yaml b/helm/chart/router/values.yaml index 8540dd2f56..35f45618a7 100644 --- a/helm/chart/router/values.yaml +++ b/helm/chart/router/values.yaml @@ -167,6 +167,8 @@ virtualservice: # gatewayNames: [] # - "gateway-1" # - "gateway-2" + # Hosts: "" # configurable but will default to '*' + # - somehost.domain.com # http: # main: # # set enabled to true to add diff --git a/licenses.html b/licenses.html index 01b9b1d8ef..37ceccad5d 100644 --- a/licenses.html +++ b/licenses.html @@ -44,16 +44,17 @@

Third Party Licenses

Overview of licenses:

All license text:

@@ -1711,6 +1712,237 @@

Used by:

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

    Apache License 2.0

    +

    Used by:

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

    Used by:

  • git2
  • hashbrown
  • hashbrown
  • +
  • hashbrown
  • hdrhistogram
  • heck
  • heck
  • @@ -9381,6 +9614,201 @@

    Used by:

    of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS + + +
  • +

    Apache License 2.0

    +

    Used by:

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

    Apache License 2.0

    Used by:

    ../../LICENSE-APACHE
    @@ -11511,7 +11940,6 @@

    Used by:

    Apache License 2.0

    Used by:

      -
    • apollo-parser
    • async-graphql-axum
    • async-graphql-derive
    • async-graphql-parser
    • @@ -11530,7 +11958,6 @@

      Used by:

    • md5
    • num-cmp
    • prost
    • -
    • rand_core
    • rhai_codegen
    • siphasher
    • system-configuration
    • @@ -12267,6 +12694,9 @@

      Elastic License 2.0

      Used by:

      Copyright 2021 Apollo Graph, Inc.
       
      @@ -16518,6 +16948,32 @@ 

      Used by:

      Except as contained in this notice, the name of a copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in these Data Files or Software without prior written authorization of the copyright holder.
      +
    • +

      zlib License

      +

      Used by:

      + +
      Copyright (c) 2024 Orson Peters
      +
      +This software is provided 'as-is', without any express or implied warranty. In
      +no event will the authors be held liable for any damages arising from the use of
      +this software.
      +
      +Permission is granted to anyone to use this software for any purpose, including
      +commercial applications, and to alter it and redistribute it freely, subject to
      +the following restrictions:
      +
      +1. The origin of this software must not be misrepresented; you must not claim
      +    that you wrote the original software. If you use this software in a product,
      +    an acknowledgment in the product documentation would be appreciated but is
      +    not required.
      +
      +2. Altered source versions must be plainly marked as such, and must not be
      +    misrepresented as being the original software.
      +
      +3. This notice may not be removed or altered from any source distribution.
      +
    diff --git a/scripts/install.sh b/scripts/install.sh index a542864a9a..6446019f5b 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -11,7 +11,7 @@ BINARY_DOWNLOAD_PREFIX="https://github.com/apollographql/router/releases/downloa # Router version defined in apollo-router's Cargo.toml # Note: Change this line manually during the release steps. -PACKAGE_VERSION="v1.57.1" +PACKAGE_VERSION="v1.58.0" download_binary() { downloader --check diff --git a/xtask/src/commands/dist.rs b/xtask/src/commands/dist.rs index 544b8e9cb7..a28dbe234c 100644 --- a/xtask/src/commands/dist.rs +++ b/xtask/src/commands/dist.rs @@ -5,13 +5,25 @@ use xtask::*; pub struct Dist { #[clap(long)] target: Option, + + /// Pass --features to cargo test + #[clap(long)] + features: Option, } impl Dist { pub fn run(&self) -> Result<()> { + let mut args = vec!["build", "--release"]; + if let Some(features) = &self.features { + args.push("--features"); + args.push(features); + } + match &self.target { Some(target) => { - cargo!(["build", "--release", "--target", target]); + args.push("--target"); + args.push(target); + cargo!(args); let bin_path = TARGET_DIR .join(target.to_string()) @@ -21,7 +33,7 @@ impl Dist { eprintln!("successfully compiled to: {}", &bin_path); } None => { - cargo!(["build", "--release"]); + cargo!(args); let bin_path = TARGET_DIR.join("release").join(RELEASE_BIN); diff --git a/xtask/src/commands/release.rs b/xtask/src/commands/release.rs index 51066382f9..db80637fc5 100644 --- a/xtask/src/commands/release.rs +++ b/xtask/src/commands/release.rs @@ -281,12 +281,12 @@ impl Prepare { fn update_docs(&self, version: &str) -> Result<()> { println!("updating docs"); replace_in_file!( - "./docs/source/containerization/docker.mdx", + "./docs/source/routing/self-hosted/containerization/docker.mdx", "with your chosen version. e.g.: `v[^`]+`", format!("with your chosen version. e.g.: `v{version}`") ); replace_in_file!( - "./docs/source/containerization/kubernetes.mdx", + "./docs/source/routing/self-hosted/containerization/kubernetes.mdx", "https://github.com/apollographql/router/tree/[^/]+/helm/chart/router", format!("https://github.com/apollographql/router/tree/v{version}/helm/chart/router") ); @@ -309,7 +309,7 @@ impl Prepare { )?; replace_in_file!( - "./docs/source/containerization/kubernetes.mdx", + "./docs/source/routing/self-hosted/containerization/kubernetes.mdx", "^```yaml\n---\n# Source: router/templates/serviceaccount.yaml(.|\n)+?```", format!("```yaml\n{}\n```", helm_chart.trim()) );