diff --git a/.changesets/chore_update_router_bridge.md b/.changesets/chore_update_router_bridge.md new file mode 100644 index 0000000000..bc23827874 --- /dev/null +++ b/.changesets/chore_update_router_bridge.md @@ -0,0 +1,8 @@ +> [!IMPORTANT] +> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), this release changes the hashing algorithm used for the cache keys. On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new hashing algorithm comes into service. + +### Update to Federation v2.9.1 ([PR #6029](https://github.com/apollographql/router/pull/6029)) + +This release updates to Federation v2.9.1, which fixes edge cases in subgraph extraction logic when using spec renaming or spec URLs (e.g., `specs.apollo.dev`) that could impact the planner's ability to plan a query. + +By [@lrlna](https://github.com/lrlna) in https://github.com/apollographql/router/pull/6027 diff --git a/.circleci/config.yml b/.circleci/config.yml index 1d0e41f62c..ac9ea0f671 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,7 @@ version: 2.1 +# TODO Remove this line. + # These "CircleCI Orbs" are reusable bits of configuration that can be shared # across projects. See https://circleci.com/orbs/ for more information. orbs: @@ -47,7 +49,7 @@ executors: # at https://circleci.com/docs/using-macos#supported-xcode-versions. # We use the major.minor notation to bring in compatible patches. xcode: "14.2.0" - resource_class: macos.m1.medium.gen1 + resource_class: macos.m1.large.gen1 macos_test: &macos_test_executor macos: # See https://circleci.com/docs/xcode-policy along with the support matrix @@ -58,7 +60,7 @@ executors: # once we update to Xcode >= 15.1.0 # See: https://github.com/apollographql/router/pull/5462 xcode: "14.2.0" - resource_class: macos.m1.medium.gen1 + resource_class: macos.m1.large.gen1 windows_build: &windows_build_executor machine: image: "windows-server-2019-vs2019:2024.02.21" @@ -574,6 +576,9 @@ jobs: parameters: platform: type: executor + fuzz: + type: boolean + default: false executor: << parameters.platform >> steps: - checkout @@ -582,7 +587,9 @@ jobs: - xtask_test - when: condition: - equal: [ *arm_linux_test_executor, << parameters.platform >> ] + and: + - equal: [ true, << parameters.fuzz >> ] + - equal: [ *arm_linux_test_executor, << parameters.platform >> ] steps: - fuzz_build @@ -971,6 +978,8 @@ workflows: platform: [ amd_linux_test ] - test: + # this should be changed back to true on dev after release + fuzz: false requires: - lint - check_helm diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e93b3042..419cee0eb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,136 @@ All notable changes to Router will be documented in this file. This project adheres to [Semantic Versioning v2.0.0](https://semver.org/spec/v2.0.0.html). +# [1.55.0] - 2024-09-24 + +> [!IMPORTANT] +> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), this release changes the hashing algorithm used for the cache keys. On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new hashing algorithm comes into service. + +## 🚀 Features + +### Support aliasing standard attributes for telemetry ([Issue #5930](https://github.com/apollographql/router/issues/5930)) + +The router now supports creating aliases for standard attributes for telemetry. + +This fixes issues where standard attribute names collide with reserved attribute names. For example, the standard attribute name `entity.type` is a [reserved attribute](https://docs.newrelic.com/docs/new-relic-solutions/new-relic-one/core-concepts/what-entity-new-relic/#reserved-attributes) name for New Relic, so it won't work properly. Moreover `entity.type` is inconsistent with our other GraphQL attributes prefixed with `graphql.` + +The example configuration below renames `entity.type` to `graphql.type.name`: + +```yaml +telemetry: + instrumentation: + spans: + mode: spec_compliant # Docs state this significantly improves performance: https://www.apollographql.com/docs/router/configuration/telemetry/instrumentation/spans#spec_compliant + instruments: + cache: # Cache instruments configuration + apollo.router.operations.entity.cache: # A counter which counts the number of cache hit and miss for subgraph requests + attributes: + graphql.type.name: # renames entity.type + alias: entity_type # ENABLED and aliased to entity_type +``` + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/5957 + +### Enable router customizations to access demand control info ([PR #5972](https://github.com/apollographql/router/pull/5972)) + +Rhai scripts and coprocessors can now access demand control information via the context. For more information on Rhai constants to access demand control info, see [available Rhai API constants](https://apollographql.com/docs/router/customizations/rhai-api#available-constants). + +By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/5972 + +### Support Redis connection pooling ([PR #5942](https://github.com/apollographql/router/pull/5942)) + +The router now supports Redis connection pooling for APQs, query planners and entity caches. This can improve performance when there is contention on Redis connections or latency in Redis calls. + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/5942 + + +## 🐛 Fixes + +### Remove unused fragments and input arguments when filtering operations ([PR #5952](https://github.com/apollographql/router/pull/5952)) + +This release fixes the authorization plugin's query filtering to remove unused fragments and input arguments if the related parts of the query are removed. Previously the plugin's query filtering generated validation errors when planning certain queries. + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/5952 + +### Hot-reloads will no longer interrupt certain gauges ([PR #5996](https://github.com/apollographql/router/pull/5996), [PR #5999](https://github.com/apollographql/router/pull/5999), [PR #5999](https://github.com/apollographql/router/pull/6012)) + +Previously when the router hot-reloaded a schema or a configuration file, the following gauges stopped working: + +* `apollo.router.cache.storage.estimated_size` +* `apollo_router_cache_size` +* `apollo.router.v8.heap.used` +* `apollo.router.v8.heap.total` +* `apollo.router.query_planning.queued` + +This issue has been fixed in this release, and the gauges now continue to function after a router hot-reloads. + +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5996 and https://github.com/apollographql/router/pull/5999 and https://github.com/apollographql/router/pull/6012 + +### Datadog sample propagation will respect previous sampling decisions ([PR #6005](https://github.com/apollographql/router/pull/6005)) + +[PR #5788](https://github.com/apollographql/router/pull/5788) introduced a regression where sampling was set on propagated headers regardless of the sampling decision in the router or upstream. + +This PR reverts the code in question and adds a test to check that a non-sampled request doesn't result in sampling in the downstream subgraph service. + +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/6005 + +### Include request variables when scoring for demand control ([PR #5995](https://github.com/apollographql/router/pull/5995)) + +Demand control scoring in the router now accounts for variables in queries. + +By [@tninesling](https://github.com/tninesling) in https://github.com/apollographql/router/pull/5995 + +## 📃 Configuration + +### Enable new and old schema introspection implementations by default ([PR #6014](https://github.com/apollographql/router/pull/6014)) + +Starting with this release, if schema introspection is enabled, the router runs both the old Javascript implementation and a new Rust implementation of its introspection logic by default. + +The more performant Rust implementation will eventually replace the Javascript implementation. For now, both implementations are run by default so we can definitively assess the reliability and stability of the Rust implementation before removing the Javascript one. + +You can still toggle between implementations using the `experimental_introspection_mode` configuration key. Its valid values: + +- `new` runs only Rust-based validation +- `legacy` runs only Javascript-based validation +- `both` (default) runs both in comparison and logs errors if differences arise + +Having `both` as the default causes no client-facing impact. It will record and output the metrics of its comparison as a `apollo.router.operations.introspection.both` counter. (Note: if this counter in your metrics has `rust_error = true` or `is_matched = false`, please open an issue with Apollo.) + +Note: schema introspection itself is disabled by default, so its implementation(s) are run only if it's enabled in your configuration: + +```yaml +supergraph: + introspection: true +``` + +By [@SimonSapin](https://github.com/SimonSapin) in https://github.com/apollographql/router/pull/6014 + +## 🧪 Experimental + +### Allow disabling persisted-queries-based query plan cache prewarm on schema reload + +The router supports the new `persisted_queries.experimental_prewarm_query_plan_cache.on_reload` configuration option. It toggles whether a query plan cache that's prewarmed upon loading a new schema includes operations from persisted query lists. Its default is `true`. Setting it `false` precludes operations from persisted query lists from being added to the prewarmed query plan cache. + +Some background about the development of this option: + +- In router v1.31.0, we started including operations from persisted query lists when the router prewarms the query plan cache when loading a new schema. + +- Then in router v1.49.0, we let you also prewarm the query plan cache from the persisted query list during router startup by setting `persisted_queries.experimental_prewarm_query_plan_cache` to true. + +- In this release, we now allow you to disable the original feature so that the router can prewarm only recent operations from the query planning cache (and not operations from persisted query lists) when loading a new schema. + +Note: the option added in v1.49.0 has been renamed from `persisted_queries.experimental_prewarm_query_plan_cache` to `persisted_queries.experimental_prewarm_query_plan_cache.on_startup`. Existing configuration files will keep working as before, but with a warning that can be resolved by updating your config file: + +```diff + persisted_queries: + enabled: true +- experimental_prewarm_query_plan_cache: true ++ experimental_prewarm_query_plan_cache: ++ on_startup: true +``` + +By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/5990 + # [1.54.0] - 2024-09-10 ## 🚀 Features @@ -23,7 +153,7 @@ telemetry: logging: stdout: enabled: true - format: + format: json: display_span_list: false span_attributes: @@ -50,7 +180,7 @@ By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router ### Add a histogram metric tracking evaluated query plans ([PR #5875](https://github.com/apollographql/router/pull/5875)) -The router supports the new `apollo.router.query_planning.plan.evaluated_plans` histogram metric to track the number of evaluated query plans. +The router supports the new `apollo.router.query_planning.plan.evaluated_plans` histogram metric to track the number of evaluated query plans. You can use it to help set an optimal `supergraph.query_planning.experimental_plans_limit` option that limits the number of query plans evaluated for a query and reduces the time spent planning. @@ -554,7 +684,7 @@ This weakness impacts all versions of Router prior to this release. See the ass ### Provide helm support for when router's health_check's default path is not being used([Issue #5652](https://github.com/apollographql/router/issues/5652)) -When helm chart is defining the liveness and readiness check probes, if the router has been configured to use a non-default health_check path, use that rather than the default ( /health ) +When helm chart is defining the liveness and readiness check probes, if the router has been configured to use a non-default health_check path, use that rather than the default ( /health ) By [Jon Christiansen](https://github.com/theJC) in https://github.com/apollographql/router/pull/5653 @@ -562,7 +692,7 @@ By [Jon Christiansen](https://github.com/theJC) in https://github.com/apollograp Metrics of the router's entity cache have been converted to the latest format with support for custom telemetry. -The following example configuration shows the the `cache` instrument, the `cache` selector in the subgraph service, and the `cache` attribute of a subgraph span: +The following example configuration shows the the `cache` instrument, the `cache` selector in the subgraph service, and the `cache` attribute of a subgraph span: ```yaml telemetry: @@ -623,7 +753,7 @@ Previously, the connection pools used by the Datadog exporter frequently timed o 2024-07-19T15:28:22.970360Z ERROR OpenTelemetry trace error occurred: error sending request for url (http://127.0.0.1:8126/v0.5/traces): connection error: Connection reset by peer (os error 54) ``` -Now, the pool timeout for the Datadog exporter has been changed so that timeout errors happen much less frequently. +Now, the pool timeout for the Datadog exporter has been changed so that timeout errors happen much less frequently. By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/5692 @@ -742,7 +872,7 @@ By [@garypen](https://github.com/garypen) in https://github.com/apollographql/ro ### Update router naming conventions ([PR #5400](https://github.com/apollographql/router/pull/5400)) Renames our router product to distinguish between our non-commercial and commercial offerings. Instead of referring to the **Apollo Router**, we now refer to the following: -- **Apollo Router Core** is Apollo’s free-and-open (ELv2 licensed) implementation of a routing runtime for supergraphs. +- **Apollo Router Core** is Apollo’s free-and-open (ELv2 licensed) implementation of a routing runtime for supergraphs. - **GraphOS Router** is based on the Apollo Router Core and fully integrated with GraphOS. GraphOS Routers provide access to GraphOS’s commercial runtime features. @@ -752,7 +882,7 @@ By [@shorgi](https://github.com/shorgi) in https://github.com/apollographql/rout ### Enable Rust-based API schema implementation ([PR #5623](https://github.com/apollographql/router/pull/5623)) -The router has transitioned to solely using a Rust-based API schema generation implementation. +The router has transitioned to solely using a Rust-based API schema generation implementation. Previously, the router used a Javascript-based implementation. After testing for a few months, we've validated the improved performance and robustness of the new Rust-based implementation, so the router now only uses it. @@ -769,7 +899,7 @@ By [@goto-bus-stop](https://github.com/goto-bus-stop) in https://github.com/apol The router now supports conditional execution of the coprocessor for each stage of the request lifecycle (except for the `Execution` stage). To configure, define conditions for a specific stage by using selectors based on headers or context entries. For example, based on a supergraph response you can configure the coprocessor not to execute for any subscription: - + ```yaml title=router.yaml @@ -777,7 +907,7 @@ coprocessor: url: http://127.0.0.1:3000 # mandatory URL which is the address of the coprocessor timeout: 2s # optional timeout (2 seconds in this example). If not set, defaults to 1 second supergraph: - response: + response: condition: not: eq: @@ -809,7 +939,7 @@ By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/p The router now supports the `subgraph_on_graphql_error` selector for the subgraph service, which it already supported for the router and supergraph services. Subgraph service support enables easier detection of GraphQL errors in response bodies of subgraph requests. -An example configuration with `subgraph_on_graphql_error` configured: +An example configuration with `subgraph_on_graphql_error` configured: ```yaml telemetry: @@ -855,7 +985,7 @@ By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router ### Provide valid trace IDs for unsampled traces in Rhai scripts ([PR #5606](https://github.com/apollographql/router/pull/5606)) -The `traceid()` function in a Rhai script for the router now returns a valid trace ID for all traces. +The `traceid()` function in a Rhai script for the router now returns a valid trace ID for all traces. Previously, `traceid()` didn't return a trace ID if the trace wasn't selected for sampling. @@ -871,7 +1001,7 @@ By [@garypen](https://github.com/garypen) in https://github.com/apollographql/ro This router now gracefully handles responses that contain invalid "`-1`" positional values for error locations in queries by ignoring those invalid locations. -This change resolves the problem of GraphQL Java and GraphQL Kotlin using `{ "line": -1, "column": -1 }` values if they can't determine an error's location in a query, but the GraphQL specification [requires both `line` and `column` to be positive numbers](https://spec.graphql.org/draft/#sel-GAPHRPFCCaCGX5zM). +This change resolves the problem of GraphQL Java and GraphQL Kotlin using `{ "line": -1, "column": -1 }` values if they can't determine an error's location in a query, but the GraphQL specification [requires both `line` and `column` to be positive numbers](https://spec.graphql.org/draft/#sel-GAPHRPFCCaCGX5zM). As an example, a subgraph can respond with invalid error locations: ```json @@ -905,7 +1035,7 @@ By [@IvanGoncharov](https://github.com/IvanGoncharov) in https://github.com/apol The router now returns request timeout errors (`408 Request Timeout`) and request rate limited errors (`429 Too Many Requests`) as structured GraphQL errors (for example, `{"errors": [...]}`). Previously, the router returned these as plaintext errors to clients. -Both types of errors are properly tracked in telemetry, including the `apollo_router_graphql_error_total` metric. +Both types of errors are properly tracked in telemetry, including the `apollo_router_graphql_error_total` metric. By [@IvanGoncharov](https://github.com/IvanGoncharov) in https://github.com/apollographql/router/pull/5578 @@ -990,7 +1120,7 @@ By [@andrewmcgivery](https://github.com/andrewmcgivery) in https://github.com/ap ### Support local persisted query manifests for use with offline licenses ([Issue #4587](https://github.com/apollographql/router/issues/4587)) -Adds experimental support for passing [persisted query manifests](https://www.apollographql.com/docs/graphos/operations/persisted-queries/#31-generate-persisted-query-manifests) to use instead of the hosted Uplink version. +Adds experimental support for passing [persisted query manifests](https://www.apollographql.com/docs/graphos/operations/persisted-queries/#31-generate-persisted-query-manifests) to use instead of the hosted Uplink version. For example: @@ -998,7 +1128,7 @@ For example: persisted_queries: enabled: true log_unknown: true - experimental_local_manifests: + experimental_local_manifests: - ./persisted-query-manifest.json safelist: enabled: true @@ -1033,7 +1163,7 @@ By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router ### Make `status_code` available for `router_service` responses in Rhai scripts ([Issue #5357](https://github.com/apollographql/router/issues/5357)) -Adds `response.status_code` on Rhai [`router_service`](https://www.apollographql.com/docs/router/customizations/rhai-api/#entry-point-hooks) responses. Previously, `status_code` was only available on `subgraph_service` responses. +Adds `response.status_code` on Rhai [`router_service`](https://www.apollographql.com/docs/router/customizations/rhai-api/#entry-point-hooks) responses. Previously, `status_code` was only available on `subgraph_service` responses. For example: @@ -1132,11 +1262,11 @@ By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/p Improves accuracy and performance of event telemetry by: -- Displaying custom event attributes even if the trace is not sampled +- Displaying custom event attributes even if the trace is not sampled - Preserving original attribute type instead of converting it to string - Ensuring `http.response.body.size` and `http.request.body.size` attributes are treated as numbers, not strings -> :warning: Exercise caution if you have monitoring enabled on your logs, as attribute types may have changed. For example, attributes like `http.response.status_code` are now numbers (`200`) instead of strings (`"200"`). +> :warning: Exercise caution if you have monitoring enabled on your logs, as attribute types may have changed. For example, attributes like `http.response.status_code` are now numbers (`200`) instead of strings (`"200"`). By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/5464 @@ -1150,7 +1280,7 @@ By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router ### Improve accuracy of `query_planning.plan.duration` ([PR #5](https://github.com/apollographql/router/pull/5530)) Previously, the `apollo.router.query_planning.plan.duration` metric inaccurately included additional processing time beyond query planning. The additional time included pooling time, which is already accounted for in the metric. After this update, apollo.router.query_planning.plan.duration now accurately reflects only the query planning duration without additional processing time. -For example, before the change, metrics reported: +For example, before the change, metrics reported: ```bash 2024-06-21T13:37:27.744592Z WARN apollo.router.query_planning.plan.duration 0.002475708 2024-06-21T13:37:27.744651Z WARN apollo.router.query_planning.total.duration 0.002553958 @@ -1218,19 +1348,19 @@ telemetry: # OLD definition of a custom instrument that measures the number of fields my.unit.instrument: value: field_unit # Changes to unit - + # NEW definition my.unit.instrument: - value: unit + value: unit - # OLD + # OLD my.custom.instrument: value: # Changes to not require `field_custom` field_custom: list_length: value # NEW my.custom.instrument: - value: + value: list_length: value ``` @@ -1359,7 +1489,7 @@ Allows HTTP/2 Cleartext (h2c) communication with coprocessors for scenarios wher Introduces a new `coprocessor.client` configuration. The first and currently only option is `experimental_http2`. The available option settings are the same as the as [`experimental_http2` traffic shaping settings](https://www.apollographql.com/docs/router/configuration/traffic-shaping/#http2). - `disable` - disable HTTP/2, use HTTP/1.1 only -- `enable` - HTTP URLs use HTTP/1.1, HTTPS URLs use TLS with either HTTP/1.1 or HTTP/2 based on the TLS handshake +- `enable` - HTTP URLs use HTTP/1.1, HTTPS URLs use TLS with either HTTP/1.1 or HTTP/2 based on the TLS handshake - `http2only` - HTTP URLs use h2c, HTTPS URLs use TLS with HTTP/2 - not set - defaults to `enable` diff --git a/Cargo.lock b/Cargo.lock index bcff62dff1..b72062f176 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,9 +159,9 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "apollo-compiler" -version = "1.0.0-beta.21" +version = "1.0.0-beta.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9496debc2b28a7da94aa6329b653fae37e0f0ec44da93d82a8d5d2a6a82abe0e" +checksum = "875f39060728ac3e775fc3fe5421225d6df92c4d5155a9524cdb198f05006d36" dependencies = [ "ahash", "apollo-parser", @@ -178,7 +178,7 @@ dependencies = [ [[package]] name = "apollo-federation" -version = "1.54.0" +version = "1.55.0" dependencies = [ "apollo-compiler", "derive_more", @@ -218,9 +218,9 @@ dependencies = [ [[package]] name = "apollo-parser" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a43dc64e71ca7140e646b99bf86ae721ebb801d2aec44e29a654c4d035ab8" +checksum = "9692c1bfa7e0628e5c46bd0538571dd469138a0f062290fd4bf90e20e46f05da" dependencies = [ "memchr", "rowan", @@ -229,7 +229,7 @@ dependencies = [ [[package]] name = "apollo-router" -version = "1.54.0" +version = "1.55.0" dependencies = [ "access-json", "ahash", @@ -250,7 +250,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-types", "axum", - "base64 0.21.7", + "base64 0.22.1", "basic-toml", "bloomfilter", "brotli 3.5.0", @@ -276,7 +276,7 @@ dependencies = [ "futures", "futures-test", "graphql_client", - "heck 0.4.1", + "heck 0.5.0", "hex", "hmac", "http 0.2.12", @@ -289,7 +289,7 @@ dependencies = [ "hyperlocal", "indexmap 2.2.6", "insta", - "itertools 0.12.1", + "itertools 0.13.0", "itoa", "jsonpath-rust", "jsonpath_lib", @@ -308,7 +308,7 @@ dependencies = [ "multer", "multimap 0.9.1", "notify", - "nu-ansi-term 0.49.0", + "nu-ansi-term 0.50.1", "num-traits", "once_cell", "opentelemetry 0.20.0", @@ -360,11 +360,10 @@ dependencies = [ "shellexpand", "similar", "static_assertions", - "strum_macros 0.25.3", + "strum_macros 0.26.4", "sys-info", "tempfile", "test-log", - "test-span", "thiserror", "tikv-jemallocator", "time", @@ -401,7 +400,7 @@ dependencies = [ [[package]] name = "apollo-router-benchmarks" -version = "1.54.0" +version = "1.55.0" dependencies = [ "apollo-parser", "apollo-router", @@ -417,7 +416,7 @@ dependencies = [ [[package]] name = "apollo-router-scaffold" -version = "1.54.0" +version = "1.55.0" dependencies = [ "anyhow", "cargo-scaffold", @@ -449,9 +448,9 @@ dependencies = [ [[package]] name = "apollo-smith" -version = "0.11.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de03c56d7feec663e7f9e981cf4570db68a0901de8c4667f5b5d20321b88af6e" +checksum = "40cff1a5989a471714cfdf53f24d0948b7f77631ab3dbd25b2f6eacbf58e5261" dependencies = [ "apollo-compiler", "apollo-parser", @@ -525,7 +524,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -698,7 +697,7 @@ dependencies = [ "proc-macro2", "quote", "strum 0.25.0", - "syn 2.0.71", + "syn 2.0.76", "thiserror", ] @@ -866,7 +865,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -883,7 +882,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -1457,7 +1456,7 @@ dependencies = [ "proc-macro2", "quote", "str_inflector", - "syn 2.0.71", + "syn 2.0.76", "thiserror", "try_match", ] @@ -1633,7 +1632,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -1989,16 +1988,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "daggy" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91a9304e55e9d601a39ae4deaba85406d5c0980e106f65afcf0460e9af1e7602" -dependencies = [ - "petgraph", - "serde", -] - [[package]] name = "darling" version = "0.20.10" @@ -2020,7 +2009,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2031,7 +2020,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2091,7 +2080,7 @@ checksum = "3c65c2ffdafc1564565200967edc4851c7b55422d3913466688907efd05ea26f" dependencies = [ "deno-proc-macro-rules-macros", "proc-macro2", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2103,7 +2092,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2158,7 +2147,7 @@ dependencies = [ "strum 0.25.0", "strum_macros 0.25.3", "syn 1.0.109", - "syn 2.0.71", + "syn 2.0.76", "thiserror", ] @@ -2239,7 +2228,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2252,7 +2241,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.0", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2290,12 +2279,6 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - [[package]] name = "digest" version = "0.10.7" @@ -2358,7 +2341,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2464,20 +2447,30 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", +] + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", ] [[package]] name = "env_logger" -version = "0.10.2" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" dependencies = [ + "anstream", + "anstyle", + "env_filter", "humantime", - "is-terminal", "log", - "regex", - "termcolor", ] [[package]] @@ -2727,7 +2720,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2895,7 +2888,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -2994,7 +2987,7 @@ checksum = "b0e085ded9f1267c32176b40921b9754c474f7dd96f7e808d4a982e48aa1e854" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -3070,9 +3063,9 @@ dependencies = [ [[package]] name = "graphql_client" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cdf7b487d864c2939b23902291a5041bc4a84418268f25fda1c8d4e15ad8fa" +checksum = "a50cfdc7f34b7f01909d55c2dcb71d4c13cbcbb4a1605d6c8bd760d654c1144b" dependencies = [ "graphql_query_derive", "serde", @@ -3081,9 +3074,9 @@ dependencies = [ [[package]] name = "graphql_client_codegen" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a40f793251171991c4eb75bd84bc640afa8b68ff6907bc89d3b712a22f700506" +checksum = "5e27ed0c2cf0c0cc52c6bcf3b45c907f433015e580879d14005386251842fb0a" dependencies = [ "graphql-introspection-query", "graphql-parser", @@ -3098,9 +3091,9 @@ dependencies = [ [[package]] name = "graphql_query_derive" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bda454f3d313f909298f626115092d348bc231025699f557b27e248475f48c" +checksum = "83febfa838f898cfa73dfaa7a8eb69ff3409021ac06ee94cfb3d622f6eeb1a97" dependencies = [ "graphql_client_codegen", "proc-macro2", @@ -3975,9 +3968,6 @@ name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" -dependencies = [ - "serde", -] [[package]] name = "linkme" @@ -3996,7 +3986,7 @@ checksum = "f8dccda732e04fa3baf2e17cf835bfe2601c7c2edafd64417c627dabae3a8cda" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -4164,14 +4154,13 @@ dependencies = [ [[package]] name = "mockall" -version = "0.11.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +checksum = "d4c28b3fb6d753d28c20e826cd46ee611fda1cf3cde03a443a974043247c065a" dependencies = [ "cfg-if", "downcast", "fragile", - "lazy_static", "mockall_derive", "predicates", "predicates-tree", @@ -4179,14 +4168,14 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.11.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +checksum = "341014e7f530314e9a1fdbc7400b244efea7122662c96bfa248c31da5bfb2020" dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.76", ] [[package]] @@ -4241,12 +4230,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - [[package]] name = "notify" version = "6.1.1" @@ -4276,11 +4259,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.49.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4853,7 +4836,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -4896,7 +4879,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -4974,7 +4957,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -5028,16 +5011,12 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "predicates" -version = "2.1.5" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" dependencies = [ - "difflib", - "float-cmp", - "itertools 0.10.5", - "normalize-line-endings", + "anstyle", "predicates-core", - "regex", ] [[package]] @@ -5189,7 +5168,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -5587,7 +5566,7 @@ checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -5630,9 +5609,9 @@ dependencies = [ [[package]] name = "router-bridge" -version = "0.6.1+v2.9.0" +version = "0.6.2+v2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3be2d3ebbcbceb19dc813f5cee507295c673bb4e3f7a7cd532c8c27c95f92fc" +checksum = "a82c217157e756750386a5371da31590c89c315d953ae6d299d73949138e332f" dependencies = [ "anyhow", "async-channel 1.9.0", @@ -5722,7 +5701,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.71", + "syn 2.0.76", "walkdir", ] @@ -5898,7 +5877,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -6007,7 +5986,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -6018,7 +5997,7 @@ checksum = "afb2522c2a87137bf6c2b3493127fed12877ef1b9476f074d6664edc98acd8a7" dependencies = [ "quote", "regex", - "syn 2.0.71", + "syn 2.0.76", "thiserror", ] @@ -6030,7 +6009,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -6152,7 +6131,7 @@ checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -6379,7 +6358,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -6392,7 +6371,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -6426,9 +6405,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.71" +version = "2.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" dependencies = [ "proc-macro2", "quote", @@ -6527,39 +6506,7 @@ checksum = "5999e24eaa32083191ba4e425deb75cdf25efefabe5aaccb7446dd0d4122a3f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", -] - -[[package]] -name = "test-span" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c12e1076fc168ae4b9870a29cabf575d318f5399f025c2248a8deae057ed12" -dependencies = [ - "daggy", - "derivative", - "indexmap 1.9.3", - "linked-hash-map", - "once_cell", - "serde", - "serde_json", - "test-span-macro", - "tokio", - "tracing", - "tracing-core", - "tracing-futures", - "tracing-subscriber", -] - -[[package]] -name = "test-span-macro" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f972445f2c781bb6d47ee4a715db3a0e404a79d977f751fd4eb2b0d44c6eb9d" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "syn 2.0.76", ] [[package]] @@ -6594,7 +6541,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -6650,9 +6597,9 @@ dependencies = [ [[package]] name = "tikv-jemalloc-sys" -version = "0.5.4+5.3.0-patched" +version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9402443cb8fd499b6f327e40565234ff34dbda27460c5b47db0db77443dd85d1" +checksum = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d" dependencies = [ "cc", "libc", @@ -6660,9 +6607,9 @@ dependencies = [ [[package]] name = "tikv-jemallocator" -version = "0.5.4" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965fe0c26be5c56c94e38ba547249074803efd52adfb66de62107d95aab3eaca" +checksum = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865" dependencies = [ "libc", "tikv-jemalloc-sys", @@ -6773,7 +6720,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -7069,7 +7016,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -7181,7 +7128,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -7263,7 +7210,7 @@ checksum = "b0a91713132798caecb23c977488945566875e7b61b902fb111979871cbff34e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] @@ -7606,7 +7553,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", "wasm-bindgen-shared", ] @@ -7640,7 +7587,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8028,7 +7975,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.76", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b332785452..0352205ebb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,15 +49,15 @@ debug = 1 # Dependencies used in more than one place are specified here in order to keep versions in sync: # https://doc.rust-lang.org/cargo/reference/workspaces.html#the-dependencies-table [workspace.dependencies] -apollo-compiler = "=1.0.0-beta.21" +apollo-compiler = "=1.0.0-beta.23" apollo-parser = "0.8.0" -apollo-smith = "0.11.0" +apollo-smith = "0.13.0" async-trait = "0.1.77" hex = { version = "0.4.3", features = ["serde"] } http = "0.2.11" insta = { version = "1.38.0", features = ["json", "redactions", "yaml"] } once_cell = "1.19.0" -reqwest = { version = "0.11.24", default-features = false, features = [ +reqwest = { version = "0.11.0", default-features = false, features = [ "rustls-tls", "rustls-native-certs", "gzip", diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md index e6c78bce10..4ba8c4326f 100644 --- a/RELEASE_CHECKLIST.md +++ b/RELEASE_CHECKLIST.md @@ -189,13 +189,7 @@ Start following the steps below to start a release PR. The process is **not ful git commit -m "prep release: v${APOLLO_ROUTER_RELEASE_VERSION}${APOLLO_ROUTER_PRERELEASE_SUFFIX}" ``` -9. Push this commit up to the existing release PR: - - ``` - git push "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "${APOLLO_ROUTER_RELEASE_VERSION}" - ``` - -10. Git tag the current commit and & push the branch and the pre-release tag simultaneously: +9. Git tag the current commit and & push the branch and the pre-release tag simultaneously: This process will kick off the bulk of the release process on CircleCI, including building each architecture on its own infrastructure and notarizing the macOS binary. @@ -204,7 +198,7 @@ Start following the steps below to start a release PR. The process is **not ful git push "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "${APOLLO_ROUTER_RELEASE_VERSION}" "v${APOLLO_ROUTER_RELEASE_VERSION}${APOLLO_ROUTER_PRERELEASE_SUFFIX}" ``` -11. Finally, publish the Crates from your local computer (this also needs to be moved to CI, but requires changing the release containers to be Rust-enabled and to restore the caches): +10. Finally, publish the Crates from your local computer (this also needs to be moved to CI, but requires changing the release containers to be Rust-enabled and to restore the caches): > Note: This command may appear unnecessarily specific, but it will help avoid publishing a version to Crates.io that doesn't match what you're currently releasing. (e.g., in the event that you've changed branches in another window) diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml index c29f03a54a..5de40a5380 100644 --- a/apollo-federation/Cargo.toml +++ b/apollo-federation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-federation" -version = "1.54.0" +version = "1.55.0" authors = ["The Apollo GraphQL Contributors"] edition = "2021" description = "Apollo Federation" diff --git a/apollo-federation/cli/src/bench.rs b/apollo-federation/cli/src/bench.rs index c672137982..ed135d8ba4 100644 --- a/apollo-federation/cli/src/bench.rs +++ b/apollo-federation/cli/src/bench.rs @@ -54,7 +54,7 @@ pub(crate) fn run_bench( } }; let now = Instant::now(); - let plan = planner.build_query_plan(&document, None); + let plan = planner.build_query_plan(&document, None, Default::default()); let elapsed = now.elapsed().as_secs_f64() * 1000.0; let mut eval_plans = None; let mut error = None; diff --git a/apollo-federation/cli/src/main.rs b/apollo-federation/cli/src/main.rs index ab42f16151..28bb5f7921 100644 --- a/apollo-federation/cli/src/main.rs +++ b/apollo-federation/cli/src/main.rs @@ -254,7 +254,10 @@ fn cmd_plan( let query_doc = ExecutableDocument::parse_and_validate(planner.api_schema().schema(), query, query_path)?; - print!("{}", planner.build_query_plan(&query_doc, None)?); + print!( + "{}", + planner.build_query_plan(&query_doc, None, Default::default())? + ); Ok(()) } diff --git a/apollo-federation/src/error/mod.rs b/apollo-federation/src/error/mod.rs index 9e4487c30f..d56c9783a6 100644 --- a/apollo-federation/src/error/mod.rs +++ b/apollo-federation/src/error/mod.rs @@ -33,8 +33,6 @@ impl From for String { #[derive(Clone, Debug, strum_macros::Display, PartialEq, Eq)] pub enum UnsupportedFeatureKind { - #[strum(to_string = "progressive overrides")] - ProgressiveOverrides, #[strum(to_string = "defer")] Defer, #[strum(to_string = "context")] diff --git a/apollo-federation/src/link/federation_spec_definition.rs b/apollo-federation/src/link/federation_spec_definition.rs index 67f181ec8b..184e93b690 100644 --- a/apollo-federation/src/link/federation_spec_definition.rs +++ b/apollo-federation/src/link/federation_spec_definition.rs @@ -12,6 +12,7 @@ use lazy_static::lazy_static; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::link::argument::directive_optional_boolean_argument; +use crate::link::argument::directive_optional_string_argument; use crate::link::argument::directive_required_string_argument; use crate::link::cost_spec_definition::CostSpecDefinition; use crate::link::cost_spec_definition::COST_VERSIONS; @@ -51,6 +52,11 @@ pub(crate) struct ProvidesDirectiveArguments<'doc> { pub(crate) fields: &'doc str, } +pub(crate) struct OverrideDirectiveArguments<'doc> { + pub(crate) from: &'doc str, + pub(crate) label: Option<&'doc str>, +} + #[derive(Debug)] pub(crate) struct FederationSpecDefinition { url: Url, @@ -361,6 +367,19 @@ impl FederationSpecDefinition { }) } + pub(crate) fn override_directive_definition<'schema>( + &self, + schema: &'schema FederationSchema, + ) -> Result<&'schema Node, FederationError> { + self.directive_definition(schema, &FEDERATION_OVERRIDE_DIRECTIVE_NAME_IN_SPEC)? + .ok_or_else(|| { + FederationError::internal(format!( + "Unexpectedly could not find federation spec's \"@{}\" directive definition", + FEDERATION_OVERRIDE_DIRECTIVE_NAME_IN_SPEC + )) + }) + } + pub(crate) fn override_directive( &self, schema: &FederationSchema, @@ -390,6 +409,19 @@ impl FederationSpecDefinition { }) } + pub(crate) fn override_directive_arguments<'doc>( + &self, + application: &'doc Node, + ) -> Result, FederationError> { + Ok(OverrideDirectiveArguments { + from: directive_required_string_argument(application, &FEDERATION_FROM_ARGUMENT_NAME)?, + label: directive_optional_string_argument( + application, + &FEDERATION_OVERRIDE_LABEL_ARGUMENT_NAME, + )?, + }) + } + pub(crate) fn get_cost_spec_definition( &self, schema: &FederationSchema, diff --git a/apollo-federation/src/operation/mod.rs b/apollo-federation/src/operation/mod.rs index 4c17083c3b..fdbfff205b 100644 --- a/apollo-federation/src/operation/mod.rs +++ b/apollo-federation/src/operation/mod.rs @@ -1324,7 +1324,7 @@ mod field_selection { } pub(crate) fn as_path_element(&self) -> FetchDataPathElement { - FetchDataPathElement::Key(self.response_name()) + FetchDataPathElement::Key(self.response_name(), Default::default()) } } @@ -2772,7 +2772,7 @@ impl SelectionSet { response_name, alias, }| { - path.push(FetchDataPathElement::Key(alias)); + path.push(FetchDataPathElement::Key(alias, Default::default())); Arc::new(FetchDataRewrite::KeyRenamer(FetchDataKeyRenamer { path, rename_key_to: response_name, @@ -2801,7 +2801,7 @@ impl SelectionSet { remaining.push(alias); } else { at_current_level.insert( - FetchDataPathElement::Key(alias.response_name.clone()), + FetchDataPathElement::Key(alias.response_name.clone(), Default::default()), alias, ); } @@ -3110,7 +3110,10 @@ fn compute_aliases_for_non_merging_fields( Some(s) => { let mut selections = s.clone(); let mut p = path.clone(); - p.push(FetchDataPathElement::Key(response_name.clone())); + p.push(FetchDataPathElement::Key( + response_name.clone(), + Default::default(), + )); selections.push(SelectionSetAtPath { path: p, selections: field.selection_set.clone(), @@ -3136,7 +3139,7 @@ fn compute_aliases_for_non_merging_fields( let selections = match field.selection_set.as_ref() { Some(s) => { let mut p = path.clone(); - p.push(FetchDataPathElement::Key(alias.clone())); + p.push(FetchDataPathElement::Key(alias.clone(), Default::default())); Some(vec![SelectionSetAtPath { path: p, selections: Some(s.clone()), @@ -3167,7 +3170,10 @@ fn compute_aliases_for_non_merging_fields( let selections: Option> = match field.selection_set.as_ref() { Some(s) => { - path.push(FetchDataPathElement::Key(response_name.clone())); + path.push(FetchDataPathElement::Key( + response_name.clone(), + Default::default(), + )); Some(vec![SelectionSetAtPath { path, selections: Some(s.clone()), diff --git a/apollo-federation/src/query_graph/build_query_graph.rs b/apollo-federation/src/query_graph/build_query_graph.rs index 3dd7abbcd6..d4d2acf609 100644 --- a/apollo-federation/src/query_graph/build_query_graph.rs +++ b/apollo-federation/src/query_graph/build_query_graph.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use apollo_compiler::collections::HashMap; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::schema::DirectiveList as ComponentDirectiveList; @@ -21,6 +22,7 @@ use crate::link::federation_spec_definition::KeyDirectiveArguments; use crate::operation::merge_selection_sets; use crate::operation::Selection; use crate::operation::SelectionSet; +use crate::query_graph::OverrideCondition; use crate::query_graph::QueryGraph; use crate::query_graph::QueryGraphEdge; use crate::query_graph::QueryGraphEdgeTransition; @@ -140,6 +142,7 @@ impl BaseQueryGraphBuilder { QueryGraphEdge { transition, conditions, + override_condition: None, }, ); let head_weight = self.query_graph.node_weight(head)?; @@ -982,6 +985,7 @@ impl FederatedQueryGraphBuilder { self.add_root_edges()?; self.handle_key()?; self.handle_requires()?; + self.handle_progressive_overrides()?; // Note that @provides must be handled last when building since it requires copying nodes // and their edges, and it's easier to reason about this if we know previous self.handle_provides()?; @@ -1374,6 +1378,102 @@ impl FederatedQueryGraphBuilder { Ok(()) } + /// Handling progressive overrides here. For each progressive @override + /// application (with a label), we want to update the edges to the overridden + /// field within the "to" and "from" subgraphs with their respective override + /// condition (the label and a T/F value). The "from" subgraph will have an + /// override condition of `false`, whereas the "to" subgraph will have an + /// override condition of `true`. + fn handle_progressive_overrides(&mut self) -> Result<(), FederationError> { + let mut edge_to_conditions: HashMap = Default::default(); + + fn collect_edge_condition( + query_graph: &QueryGraph, + target_graph: &str, + target_field: &ObjectFieldDefinitionPosition, + label: &str, + condition: bool, + edge_to_conditions: &mut HashMap, + ) -> Result<(), FederationError> { + let target_field = FieldDefinitionPosition::Object(target_field.clone()); + let subgraph_nodes = query_graph + .types_to_nodes_by_source + .get(target_graph) + .unwrap(); + let parent_node = subgraph_nodes + .get(target_field.type_name()) + .unwrap() + .first() + .unwrap(); + for edge in query_graph.out_edges(*parent_node) { + let edge_weight = query_graph.edge_weight(edge.id())?; + let QueryGraphEdgeTransition::FieldCollection { + field_definition_position, + .. + } = &edge_weight.transition + else { + continue; + }; + + if &target_field == field_definition_position { + edge_to_conditions.insert( + edge.id(), + OverrideCondition { + label: label.to_string(), + condition, + }, + ); + } + } + Ok(()) + } + + for (to_subgraph_name, subgraph) in &self.base.query_graph.subgraphs_by_name { + let subgraph_data = self.subgraphs.get(to_subgraph_name)?; + if let Some(override_referencers) = subgraph + .referencers() + .directives + .get(&subgraph_data.overrides_directive_definition_name) + { + for field_definition_position in &override_referencers.object_fields { + let field = field_definition_position.get(subgraph.schema())?; + for directive in field + .directives + .get_all(&subgraph_data.overrides_directive_definition_name) + { + let application = subgraph_data + .federation_spec_definition + .override_directive_arguments(directive)?; + if let Some(label) = application.label { + collect_edge_condition( + &self.base.query_graph, + to_subgraph_name, + field_definition_position, + label, + true, + &mut edge_to_conditions, + )?; + collect_edge_condition( + &self.base.query_graph, + application.from, + field_definition_position, + label, + false, + &mut edge_to_conditions, + )?; + } + } + } + } + } + + for (edge, condition) in edge_to_conditions { + let mutable_edge = self.base.query_graph.edge_weight_mut(edge)?; + mutable_edge.override_condition = Some(condition); + } + Ok(()) + } + /// Handle @provides by copying the appropriate nodes/edges. fn handle_provides(&mut self) -> Result<(), FederationError> { let mut provide_id = 0; @@ -1987,6 +2087,10 @@ impl FederatedQueryGraphBuilderSubgraphs { ), } })?; + let overrides_directive_definition_name = federation_spec_definition + .override_directive_definition(schema)? + .name + .clone(); subgraphs.map.insert( source.clone(), FederatedQueryGraphBuilderSubgraphData { @@ -1995,6 +2099,7 @@ impl FederatedQueryGraphBuilderSubgraphs { requires_directive_definition_name, provides_directive_definition_name, interface_object_directive_definition_name, + overrides_directive_definition_name, }, ); } @@ -2020,6 +2125,7 @@ struct FederatedQueryGraphBuilderSubgraphData { requires_directive_definition_name: Name, provides_directive_definition_name: Name, interface_object_directive_definition_name: Name, + overrides_directive_definition_name: Name, } #[derive(Debug)] diff --git a/apollo-federation/src/query_graph/graph_path.rs b/apollo-federation/src/query_graph/graph_path.rs index b9486f8434..88bba9e8a0 100644 --- a/apollo-federation/src/query_graph/graph_path.rs +++ b/apollo-federation/src/query_graph/graph_path.rs @@ -46,6 +46,7 @@ use crate::query_graph::path_tree::OpPathTree; use crate::query_graph::QueryGraph; use crate::query_graph::QueryGraphEdgeTransition; use crate::query_graph::QueryGraphNodeType; +use crate::query_plan::query_planner::EnabledOverrideConditions; use crate::query_plan::FetchDataPathElement; use crate::query_plan::QueryPathElement; use crate::query_plan::QueryPlanCost; @@ -1406,17 +1407,24 @@ where feature = "snapshot_tracing", tracing::instrument(skip_all, level = "trace") )] + #[allow(clippy::too_many_arguments)] fn advance_with_non_collecting_and_type_preserving_transitions( self: &Arc, context: &OpGraphPathContext, condition_resolver: &mut impl ConditionResolver, excluded_destinations: &ExcludedDestinations, excluded_conditions: &ExcludedConditions, + override_conditions: &EnabledOverrideConditions, transition_and_context_to_trigger: impl Fn( &QueryGraphEdgeTransition, &OpGraphPathContext, ) -> TTrigger, - node_and_trigger_to_edge: impl Fn(&Arc, NodeIndex, &Arc) -> Option, + node_and_trigger_to_edge: impl Fn( + &Arc, + NodeIndex, + &Arc, + &EnabledOverrideConditions, + ) -> Option, ) -> Result, FederationError> { // If we're asked for indirect paths after an "@interfaceObject fake down cast" but that // down cast comes just after non-collecting edge(s), then we can ignore the ask (skip @@ -1675,6 +1683,7 @@ where direct_path_start_node, edge_tail_type_pos, &node_and_trigger_to_edge, + override_conditions, )? } else { None @@ -1797,12 +1806,20 @@ where start_index: usize, start_node: NodeIndex, end_type_position: &OutputTypeDefinitionPosition, - node_and_trigger_to_edge: impl Fn(&Arc, NodeIndex, &Arc) -> Option, + node_and_trigger_to_edge: impl Fn( + &Arc, + NodeIndex, + &Arc, + &EnabledOverrideConditions, + ) -> Option, + override_conditions: &EnabledOverrideConditions, ) -> Result, FederationError> { let mut current_node = start_node; for index in start_index..self.edges.len() { let trigger = &self.edge_triggers[index]; - let Some(edge) = node_and_trigger_to_edge(&self.graph, current_node, trigger) else { + let Some(edge) = + node_and_trigger_to_edge(&self.graph, current_node, trigger, override_conditions) + else { return Ok(None); }; @@ -1892,8 +1909,13 @@ where } impl OpGraphPath { - fn next_edge_for_field(&self, field: &Field) -> Option { - self.graph.edge_for_field(self.tail, field) + fn next_edge_for_field( + &self, + field: &Field, + override_conditions: &EnabledOverrideConditions, + ) -> Option { + self.graph + .edge_for_field(self.tail, field, override_conditions) } fn next_edge_for_inline_fragment(&self, inline_fragment: &InlineFragment) -> Option { @@ -2027,6 +2049,7 @@ impl OpGraphPath { pub(crate) fn terminate_with_non_requested_typename_field( &self, + override_conditions: &EnabledOverrideConditions, ) -> Result { // If the last step of the path was a fragment/type-condition, we want to remove it before // we get __typename. The reason is that this avoid cases where this method would make us @@ -2066,7 +2089,10 @@ impl OpGraphPath { &tail_type_pos, None, ); - let Some(edge) = self.graph.edge_for_field(path.tail, &typename_field) else { + let Some(edge) = self + .graph + .edge_for_field(path.tail, &typename_field, override_conditions) + else { return Err(FederationError::internal( "Unexpectedly missing edge for __typename field", )); @@ -2169,7 +2195,7 @@ impl OpGraphPath { let Some(self_edge) = self.edges[diff_pos] else { return Ok(false); }; - let Some(other_edge) = self.edges[diff_pos] else { + let Some(other_edge) = other.edges[diff_pos] else { return Ok(false); }; let other_edge_weight = other.graph.edge_weight(other_edge)?; @@ -2190,7 +2216,7 @@ impl OpGraphPath { // from the interface while the other one from an implementation, they won't be technically // the "same" edge index. So we check that both are key-resolution edges, to the same // subgraph and type, and with the same condition. - let Some(other_next_edge) = self.edges[diff_pos + 1] else { + let Some(other_next_edge) = other.edges[diff_pos + 1] else { return Ok(false); }; let (_, self_edge_tail) = other.graph.edge_endpoints(self_edge)?; @@ -2401,6 +2427,7 @@ impl OpGraphPath { operation_element: &OpPathElement, context: &OpGraphPathContext, condition_resolver: &mut impl ConditionResolver, + override_conditions: &EnabledOverrideConditions, ) -> Result<(Option>, Option), FederationError> { let span = debug_span!("Trying to advance {self} directly with {operation_element}"); let _guard = span.enter(); @@ -2417,7 +2444,9 @@ impl OpGraphPath { OutputTypeDefinitionPosition::Object(tail_type_pos) => { // Just take the edge corresponding to the field, if it exists and can be // used. - let Some(edge) = self.next_edge_for_field(operation_field) else { + let Some(edge) = + self.next_edge_for_field(operation_field, override_conditions) + else { debug!( "No edge for field {operation_field} on object type {tail_weight}" ); @@ -2504,7 +2533,7 @@ impl OpGraphPath { let interface_edge = if field_is_of_an_implementation { None } else { - self.next_edge_for_field(operation_field) + self.next_edge_for_field(operation_field, override_conditions) }; let interface_path = if let Some(interface_edge) = &interface_edge { let field_path = self.add_field_edge( @@ -2668,6 +2697,7 @@ impl OpGraphPath { supergraph_schema.clone(), &implementation_inline_fragment.into(), condition_resolver, + override_conditions, )?; // If we find no options for that implementation, we bail (as we need to // simultaneously advance all implementations). @@ -2697,6 +2727,7 @@ impl OpGraphPath { supergraph_schema.clone(), operation_element, condition_resolver, + override_conditions, )?; let Some(field_options_for_implementation) = field_options_for_implementation @@ -2756,7 +2787,9 @@ impl OpGraphPath { } } OutputTypeDefinitionPosition::Union(_) => { - let Some(typename_edge) = self.next_edge_for_field(operation_field) else { + let Some(typename_edge) = + self.next_edge_for_field(operation_field, override_conditions) + else { return Err(FederationError::internal( "Should always have an edge for __typename edge on an union", )); @@ -2871,6 +2904,7 @@ impl OpGraphPath { supergraph_schema.clone(), &implementation_inline_fragment.into(), condition_resolver, + override_conditions, )?; let Some(implementation_options) = implementation_options else { drop(guard); @@ -3277,6 +3311,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { updated_context: &OpGraphPathContext, path_index: usize, condition_resolver: &mut impl ConditionResolver, + override_conditions: &EnabledOverrideConditions, ) -> Result { // Note that the provided context will usually be one we had during construction (the // `updated_context` will be `self.context` updated by whichever operation we're looking at, @@ -3284,12 +3319,13 @@ impl SimultaneousPathsWithLazyIndirectPaths { // rare), which is why we save recomputation by caching the computed value in that case, but // in case it's different, we compute without caching. if *updated_context != self.context { - self.compute_indirect_paths(path_index, condition_resolver)?; + self.compute_indirect_paths(path_index, condition_resolver, override_conditions)?; } if let Some(indirect_paths) = &self.lazily_computed_indirect_paths[path_index] { Ok(indirect_paths.clone()) } else { - let new_indirect_paths = self.compute_indirect_paths(path_index, condition_resolver)?; + let new_indirect_paths = + self.compute_indirect_paths(path_index, condition_resolver, override_conditions)?; self.lazily_computed_indirect_paths[path_index] = Some(new_indirect_paths.clone()); Ok(new_indirect_paths) } @@ -3299,17 +3335,21 @@ impl SimultaneousPathsWithLazyIndirectPaths { &self, path_index: usize, condition_resolver: &mut impl ConditionResolver, + overridden_conditions: &EnabledOverrideConditions, ) -> Result { self.paths.0[path_index].advance_with_non_collecting_and_type_preserving_transitions( &self.context, condition_resolver, &self.excluded_destinations, &self.excluded_conditions, + overridden_conditions, // The transitions taken by this method are non-collecting transitions, in which case // the trigger is the context (which is really a hack to provide context information for // keys during fetch dependency graph updating). |_, context| OpGraphPathTrigger::Context(context.clone()), - |graph, node, trigger| graph.edge_for_op_graph_path_trigger(node, trigger), + |graph, node, trigger, overridden_conditions| { + graph.edge_for_op_graph_path_trigger(node, trigger, overridden_conditions) + }, ) } @@ -3345,6 +3385,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { supergraph_schema: ValidFederationSchema, operation_element: &OpPathElement, condition_resolver: &mut impl ConditionResolver, + override_conditions: &EnabledOverrideConditions, ) -> Result>, FederationError> { debug!( "Trying to advance paths for operation: path = {}, operation = {operation_element}", @@ -3375,6 +3416,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { operation_element, &updated_context, condition_resolver, + override_conditions, )?; debug!("{advance_options:?}"); drop(gaurd); @@ -3422,25 +3464,31 @@ impl SimultaneousPathsWithLazyIndirectPaths { if let OpPathElement::Field(operation_field) = operation_element { // Add whatever options can be obtained by taking some non-collecting edges first. let paths_with_non_collecting_edges = self - .indirect_options(&updated_context, path_index, condition_resolver)? + .indirect_options( + &updated_context, + path_index, + condition_resolver, + override_conditions, + )? .filter_non_collecting_paths_for_field(operation_field)?; if !paths_with_non_collecting_edges.paths.is_empty() { debug!( "{} indirect paths", paths_with_non_collecting_edges.paths.len() ); - for paths_with_non_collecting_edges in + for path_with_non_collecting_edges in paths_with_non_collecting_edges.paths.iter() { - debug!("For indirect path {paths_with_non_collecting_edges}:"); + debug!("For indirect path {path_with_non_collecting_edges}:"); let span = debug_span!(" |"); let _gaurd = span.enter(); - let (advance_options, _) = paths_with_non_collecting_edges + let (advance_options, _) = path_with_non_collecting_edges .advance_with_operation_element( supergraph_schema.clone(), operation_element, &updated_context, condition_resolver, + override_conditions, )?; // If we can't advance the operation element after that path, ignore it, // it's just not an option. @@ -3458,7 +3506,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { if advance_options.is_empty() { return Err(FederationError::internal(format!( "Unexpected empty options after non-collecting path {} for {}", - paths_with_non_collecting_edges, operation_element, + path_with_non_collecting_edges, operation_element, ))); } // There is a special case we can deal with now. Namely, suppose we have a @@ -3485,7 +3533,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { // the new path renders unnecessary. Do note that we only make that check // when the new option is a single-path option, because this gets kind of // complicated otherwise. - if paths_with_non_collecting_edges.tail_is_interface_object()? { + if path_with_non_collecting_edges.tail_is_interface_object()? { for indirect_option in &advance_options { if indirect_option.0.len() == 1 { let mut new_options = vec![]; @@ -3528,6 +3576,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { operation_element, &updated_context, condition_resolver, + override_conditions, )?; options = advance_options.unwrap_or_else(Vec::new); debug!("{options:?}"); @@ -3552,11 +3601,6 @@ impl SimultaneousPathsWithLazyIndirectPaths { // PORT_NOTE: JS passes a ConditionResolver here, we do not: see port note for // `SimultaneousPathsWithLazyIndirectPaths` -// TODO(@goto-bus-stop): JS passes `override_conditions` here and maintains stores -// references to it in the created paths. AFAICT override conditions -// are shared mutable state among different query graphs, so having references to -// it in many structures would require synchronization. We should likely pass it as -// an argument to exactly the functionality that uses it. pub fn create_initial_options( initial_path: GraphPath>, initial_type: &QueryGraphNodeType, @@ -3564,6 +3608,7 @@ pub fn create_initial_options( condition_resolver: &mut impl ConditionResolver, excluded_edges: ExcludedDestinations, excluded_conditions: ExcludedConditions, + override_conditions: &EnabledOverrideConditions, ) -> Result, FederationError> { let initial_paths = SimultaneousPaths::from(initial_path); let mut lazy_initial_path = SimultaneousPathsWithLazyIndirectPaths::new( @@ -3574,8 +3619,12 @@ pub fn create_initial_options( ); if initial_type.is_federated_root_type() { - let initial_options = - lazy_initial_path.indirect_options(&initial_context, 0, condition_resolver)?; + let initial_options = lazy_initial_path.indirect_options( + &initial_context, + 0, + condition_resolver, + override_conditions, + )?; let options = initial_options .paths .iter() diff --git a/apollo-federation/src/query_graph/mod.rs b/apollo-federation/src/query_graph/mod.rs index f95588446c..4060ffdbe6 100644 --- a/apollo-federation/src/query_graph/mod.rs +++ b/apollo-federation/src/query_graph/mod.rs @@ -44,6 +44,7 @@ use crate::query_graph::graph_path::ExcludedDestinations; use crate::query_graph::graph_path::OpGraphPathContext; use crate::query_graph::graph_path::OpGraphPathTrigger; use crate::query_graph::graph_path::OpPathElement; +use crate::query_plan::query_planner::EnabledOverrideConditions; use crate::query_plan::QueryPlanCost; #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -147,6 +148,25 @@ pub(crate) struct QueryGraphEdge { /// /// Outside of keys, @requires edges also rely on conditions. pub(crate) conditions: Option>, + /// Edges can require that an override condition (provided during query + /// planning) be met in order to be taken. This is used for progressive + /// @override, where (at least) 2 subgraphs can resolve the same field, but + /// one of them has an @override with a label. If the override condition + /// matches the query plan parameters, this edge can be taken. + pub(crate) override_condition: Option, +} + +impl QueryGraphEdge { + fn satisfies_override_conditions( + &self, + conditions_to_check: &EnabledOverrideConditions, + ) -> bool { + if let Some(override_condition) = &self.override_condition { + override_condition.condition == conditions_to_check.contains(&override_condition.label) + } else { + true + } + } } impl Display for QueryGraphEdge { @@ -158,13 +178,32 @@ impl Display for QueryGraphEdge { { return Ok(()); } - if let Some(conditions) = &self.conditions { - write!(f, "{} ⊢ {}", conditions, self.transition) - } else { - self.transition.fmt(f) + + match (&self.override_condition, &self.conditions) { + (Some(override_condition), Some(conditions)) => write!( + f, + "{}, {} ⊢ {}", + conditions, override_condition, self.transition + ), + (Some(override_condition), None) => { + write!(f, "{} ⊢ {}", override_condition, self.transition) + } + (None, Some(conditions)) => write!(f, "{} ⊢ {}", conditions, self.transition), + _ => self.transition.fmt(f), } } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct OverrideCondition { + pub(crate) label: String, + pub(crate) condition: bool, +} + +impl Display for OverrideCondition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{} = {}", self.label, self.condition) + } +} /// The type of query graph edge "transition". /// @@ -639,7 +678,12 @@ impl QueryGraph { .find_ok(|selection| !external_metadata.selects_any_external_field(selection)) } - pub(crate) fn edge_for_field(&self, node: NodeIndex, field: &Field) -> Option { + pub(crate) fn edge_for_field( + &self, + node: NodeIndex, + field: &Field, + override_conditions: &EnabledOverrideConditions, + ) -> Option { let mut candidates = self.out_edges(node).into_iter().filter_map(|edge_ref| { let edge_weight = edge_ref.weight(); let QueryGraphEdgeTransition::FieldCollection { @@ -649,6 +693,11 @@ impl QueryGraph { else { return None; }; + + if !edge_weight.satisfies_override_conditions(override_conditions) { + return None; + } + // We explicitly avoid comparing parent type's here, to allow interface object // fields to match operation fields with the same name but differing types. if field.field_position.field_name() == field_definition_position.field_name() { @@ -715,12 +764,15 @@ impl QueryGraph { &self, node: NodeIndex, op_graph_path_trigger: &OpGraphPathTrigger, + override_conditions: &EnabledOverrideConditions, ) -> Option> { let OpGraphPathTrigger::OpPathElement(op_path_element) = op_graph_path_trigger else { return None; }; match op_path_element { - OpPathElement::Field(field) => self.edge_for_field(node, field).map(Some), + OpPathElement::Field(field) => self + .edge_for_field(node, field, override_conditions) + .map(Some), OpPathElement::InlineFragment(inline_fragment) => { if inline_fragment.type_condition_position.is_some() { self.edge_for_inline_fragment(node, inline_fragment) diff --git a/apollo-federation/src/query_plan/display.rs b/apollo-federation/src/query_plan/display.rs index 9141b87747..a00efef669 100644 --- a/apollo-federation/src/query_plan/display.rs +++ b/apollo-federation/src/query_plan/display.rs @@ -356,13 +356,27 @@ fn write_selections( impl fmt::Display for FetchDataPathElement { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Key(name) => f.write_str(name), - Self::AnyIndex => f.write_str("@"), + Self::Key(name, conditions) => { + f.write_str(name)?; + write_conditions(conditions, f) + } + Self::AnyIndex(conditions) => { + f.write_str("@")?; + write_conditions(conditions, f) + } Self::TypenameEquals(name) => write!(f, "... on {name}"), } } } +fn write_conditions(conditions: &[Name], f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !conditions.is_empty() { + write!(f, "|[{}]", conditions.join(",")) + } else { + Ok(()) + } +} + impl fmt::Display for QueryPathElement { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { diff --git a/apollo-federation/src/query_plan/fetch_dependency_graph.rs b/apollo-federation/src/query_plan/fetch_dependency_graph.rs index de9a3a87c3..ad2134aaff 100644 --- a/apollo-federation/src/query_plan/fetch_dependency_graph.rs +++ b/apollo-federation/src/query_plan/fetch_dependency_graph.rs @@ -68,6 +68,7 @@ use crate::schema::position::CompositeTypeDefinitionPosition; use crate::schema::position::FieldDefinitionPosition; use crate::schema::position::ObjectTypeDefinitionPosition; use crate::schema::position::OutputTypeDefinitionPosition; +use crate::schema::position::PositionLookupError; use crate::schema::position::SchemaRootDefinitionKind; use crate::schema::position::TypeDefinitionPosition; use crate::schema::ValidFederationSchema; @@ -203,7 +204,7 @@ type FetchDependencyGraphPetgraph = pub(crate) struct FetchDependencyGraph { /// The supergraph schema that generated the federated query graph. #[serde(skip)] - supergraph_schema: ValidFederationSchema, + pub(crate) supergraph_schema: ValidFederationSchema, /// The federated query graph that generated the fetches. (This also contains the subgraph /// schemas.) #[serde(skip)] @@ -251,11 +252,15 @@ pub(crate) struct DeferredInfo { } // TODO: Write docstrings -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub(crate) struct FetchDependencyGraphNodePath { + schema: ValidFederationSchema, pub(crate) full_path: Arc, path_in_node: Arc, response_path: Vec, + type_conditioned_fetching_enabled: bool, + possible_types: IndexSet, + possible_types_after_last_field: IndexSet, } #[derive(Debug, Clone)] @@ -443,11 +448,40 @@ impl ProcessingState { } impl FetchDependencyGraphNodePath { + pub(crate) fn new( + schema: ValidFederationSchema, + type_conditioned_fetching_enabled: bool, + root_type: CompositeTypeDefinitionPosition, + ) -> Result { + let root_possible_types = if type_conditioned_fetching_enabled { + schema.possible_runtime_types(root_type)? + } else { + Default::default() + } + .into_iter() + .map(|pos| Ok(pos.get(schema.schema())?.name.clone())) + .collect::, _>>() + .map_err(|e: PositionLookupError| FederationError::from(e))?; + + Ok(Self { + schema, + type_conditioned_fetching_enabled, + full_path: Default::default(), + path_in_node: Default::default(), + response_path: Default::default(), + possible_types: root_possible_types.clone(), + possible_types_after_last_field: root_possible_types, + }) + } fn for_new_key_fetch(&self, new_context: Arc) -> Self { Self { + schema: self.schema.clone(), full_path: self.full_path.clone(), path_in_node: new_context, response_path: self.response_path.clone(), + type_conditioned_fetching_enabled: self.type_conditioned_fetching_enabled, + possible_types: self.possible_types.clone(), + possible_types_after_last_field: self.possible_types_after_last_field.clone(), } } @@ -455,33 +489,146 @@ impl FetchDependencyGraphNodePath { &self, element: Arc, ) -> Result { + let response_path = self.updated_response_path(&element)?; + let new_possible_types = self.new_possible_types(&element)?; + let possible_types_after_last_field = if let &OpPathElement::Field(_) = element.as_ref() { + new_possible_types.clone() + } else { + self.possible_types_after_last_field.clone() + }; + Ok(Self { - response_path: self.updated_response_path(&element)?, + schema: self.schema.clone(), + response_path, full_path: Arc::new(self.full_path.with_pushed(element.clone())), path_in_node: Arc::new(self.path_in_node.with_pushed(element)), + type_conditioned_fetching_enabled: self.type_conditioned_fetching_enabled, + possible_types: new_possible_types, + possible_types_after_last_field, }) } + fn new_possible_types( + &self, + element: &OpPathElement, + ) -> Result, FederationError> { + if !self.type_conditioned_fetching_enabled { + return Ok(Default::default()); + } + + let res = match element { + OpPathElement::InlineFragment(f) => match &f.type_condition_position { + None => self.possible_types.clone(), + Some(tcp) => { + let element_possible_types = self.schema.possible_runtime_types(tcp.clone())?; + element_possible_types + .iter() + .filter(|&possible_type| { + self.possible_types.contains(&possible_type.type_name) + }) + .map(|possible_type| possible_type.type_name.clone()) + .collect() + } + }, + OpPathElement::Field(f) => self.advance_field_type(f)?, + }; + Ok(res) + } + + fn advance_field_type(&self, element: &Field) -> Result, FederationError> { + if !element + .data() + .output_base_type() + .map(|base_type| base_type.is_composite_type()) + .unwrap_or_default() + { + return Ok(Default::default()); + } + + let mut res = self + .possible_types + .clone() + .into_iter() + .map(|pt| { + let field = CompositeTypeDefinitionPosition::try_from(self.schema.get_type(pt)?)? + .field(element.name().clone())? + .get(self.schema.schema())?; + let typ = self + .schema + .get_type(field.ty.inner_named_type().clone())? + .try_into()?; + Ok(self + .schema + .possible_runtime_types(typ)? + .into_iter() + .map(|ctdp| ctdp.type_name) + .collect::>()) + }) + .collect::>, FederationError>>()? + .into_iter() + .flatten() + .collect::>(); + + res.sort(); + + Ok(res.into_iter().collect()) + } + fn updated_response_path( &self, element: &OpPathElement, ) -> Result, FederationError> { let mut new_path = self.response_path.clone(); - if let OpPathElement::Field(field) = element { - new_path.push(FetchDataPathElement::Key(field.response_name())); - // TODO: is there a simpler we to find a field’s type from `&Field`? - let mut type_ = &field.field_position.get(field.schema.schema())?.ty; - loop { - match type_ { - schema::Type::Named(_) | schema::Type::NonNullNamed(_) => break, - schema::Type::List(inner) | schema::Type::NonNullList(inner) => { - new_path.push(FetchDataPathElement::AnyIndex); - type_ = inner + match element { + OpPathElement::InlineFragment(_) => Ok(new_path), + OpPathElement::Field(field) => { + // Type conditions on the last element of a path don't imply different subgraph fetches. + // They would only translate to a potentially new fragment. + // So instead of applying a type condition to the last element of a path + // We keep track of type conditions and apply them to the parent if applicable. + // EG: + // foo.bar|[baz, qux] # |[baz, qux] aren't necessary + // foo.bar|[baz, qux].quux # |[baz, qux] apply to the parents, they are necessary + if self.possible_types_after_last_field.len() != self.possible_types.len() { + let conditions = &self.possible_types; + + match new_path.pop() { + Some(FetchDataPathElement::AnyIndex(_)) => { + new_path.push(FetchDataPathElement::AnyIndex( + conditions.iter().cloned().collect(), + )); + } + Some(FetchDataPathElement::Key(name, _)) => { + new_path.push(FetchDataPathElement::Key( + name, + conditions.iter().cloned().collect(), + )); + } + Some(other) => new_path.push(other), + None => {} } } + + new_path.push(FetchDataPathElement::Key( + field.response_name(), + Default::default(), + )); + + // TODO: is there a simpler way to find a field’s type from `&Field`? + let mut type_ = &field.field_position.get(field.schema.schema())?.ty; + loop { + match type_ { + schema::Type::Named(_) | schema::Type::NonNullNamed(_) => break, + schema::Type::List(inner) | schema::Type::NonNullList(inner) => { + new_path.push(FetchDataPathElement::AnyIndex(Default::default())); + type_ = inner + } + } + } + + Ok(new_path) } - }; - Ok(new_path) + } } } @@ -1346,7 +1493,7 @@ impl FetchDependencyGraph { input_selection_set.contains(sub_selection_set) })) } else if !implementation_input_selections.is_empty() { - Ok(interface_input_selections.iter().all(|input| { + Ok(implementation_input_selections.iter().all(|input| { let Some(input_selection_set) = input.selection_set() else { return false; }; @@ -2478,6 +2625,38 @@ impl FetchDependencyGraphNode { _ => err, })?; + // this function removes unnecessary pieces of the query plan requires selection set. + // PORT NOTE: this function was called trimSelectioNodes in the JS implementation + fn trim_requires_selection_set( + selection_set: &executable::SelectionSet, + ) -> Vec { + selection_set + .selections + .iter() + .filter_map(|s| match s { + executable::Selection::Field(field) => Some(executable::Selection::from( + executable::Field::new(field.name.clone(), field.definition.clone()) + .with_selections(trim_requires_selection_set(&field.selection_set)), + )), + executable::Selection::InlineFragment(inline_fragment) => { + let new_fragment = inline_fragment + .type_condition + .clone() + .map(executable::InlineFragment::with_type_condition) + .unwrap_or_else(|| { + executable::InlineFragment::without_type_condition( + inline_fragment.selection_set.ty.clone(), + ) + }) + .with_selections(trim_requires_selection_set( + &inline_fragment.selection_set, + )); + Some(executable::Selection::from(new_fragment)) + } + executable::Selection::FragmentSpread(_) => None, + }) + .collect() + } let node = super::PlanNode::Fetch(Box::new(super::FetchNode { subgraph_name: self.subgraph_name.clone(), id: self.id.get().copied(), @@ -2486,7 +2665,7 @@ impl FetchDependencyGraphNode { .as_ref() .map(executable::SelectionSet::try_from) .transpose()? - .map(|selection_set| selection_set.selections), + .map(|selection_set| trim_requires_selection_set(&selection_set)), operation_document, operation_name, operation_kind: self.root_kind.into(), @@ -2881,6 +3060,9 @@ impl FetchSelectionSet { fn add_selections(&mut self, selection_set: &Arc) -> Result<(), FederationError> { Arc::make_mut(&mut self.selection_set).add_selection_set(selection_set)?; + // TODO: when calling this multiple times, maybe only re-compute conditions at the end? + // Or make it lazily-initialized and computed on demand? + self.conditions = self.selection_set.conditions()?; Ok(()) } } @@ -3798,7 +3980,7 @@ fn compute_input_rewrites_on_key_fetch( { // rewrite path: [ ... on , __typename ] let type_cond = FetchDataPathElement::TypenameEquals(input_type_name.clone()); - let typename_field_elem = FetchDataPathElement::Key(TYPENAME_FIELD); + let typename_field_elem = FetchDataPathElement::Key(TYPENAME_FIELD, Default::default()); let rewrite = FetchDataRewrite::ValueSetter(FetchDataValueSetter { path: vec![type_cond, typename_field_elem], set_value_to: dest_type.type_name().to_string().into(), @@ -4415,8 +4597,225 @@ fn path_for_parent( let filtered_path = path.path_in_node.filter_on_schema(parent_schema); let final_path = concat_op_paths(parent_path.deref(), &filtered_path); Ok(FetchDependencyGraphNodePath { + schema: dependency_graph.supergraph_schema.clone(), full_path: path.full_path.clone(), path_in_node: Arc::new(final_path), response_path: path.response_path.clone(), + possible_types: path.possible_types.clone(), + possible_types_after_last_field: path.possible_types_after_last_field.clone(), + type_conditioned_fetching_enabled: path.type_conditioned_fetching_enabled, }) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::schema::position::InterfaceTypeDefinitionPosition; + + #[test] + fn type_condition_fetching_disabled() { + let schema = apollo_compiler::Schema::parse_and_validate( + r#" + type Query { + foo: Foo + } + interface Foo { + bar: Bar + } + interface Bar { + baz: String + } + type Foo_1 implements Foo { + bar: Bar_1 + a: Int + } + type Foo_2 implements Foo { + bar: Bar_2 + b: Int + } + type Bar_1 implements Bar { + baz: String + a: Int + } + type Bar_2 implements Bar { + baz: String + b: Int + } + type Bar_3 implements Bar { + baz: String + } + "#, + "schema.graphql", + ) + .unwrap(); + + let valid_schema = ValidFederationSchema::new(schema.clone()).unwrap(); + + let foo = object_field_element(&valid_schema, name!("Query"), name!("foo")); + let frag = inline_fragment_element(&valid_schema, name!("Foo"), Some(name!("Foo_1"))); + let bar = object_field_element(&valid_schema, name!("Foo_1"), name!("bar")); + let frag2 = inline_fragment_element(&valid_schema, name!("Bar"), Some(name!("Bar_1"))); + let baz = object_field_element(&valid_schema, name!("Bar_1"), name!("baz")); + + let query_root = valid_schema + .get_type(name!("Query")) + .unwrap() + .try_into() + .unwrap(); + + let path = FetchDependencyGraphNodePath::new(valid_schema, false, query_root).unwrap(); + + let path = path.add(Arc::new(foo)).unwrap(); + let path = path.add(Arc::new(frag)).unwrap(); + let path = path.add(Arc::new(bar)).unwrap(); + let path = path.add(Arc::new(frag2)).unwrap(); + let path = path.add(Arc::new(baz)).unwrap(); + + assert_eq!(".foo.bar.baz", &to_string(&path.response_path)); + } + + #[test] + fn type_condition_fetching_enabled() { + let schema = apollo_compiler::Schema::parse_and_validate( + r#" + type Query { + foo: Foo + } + interface Foo { + bar: Bar + } + interface Bar { + baz: String + } + type Foo_1 implements Foo { + bar: Bar_1 + a: Int + } + type Foo_2 implements Foo { + bar: Bar_2 + b: Int + } + type Bar_1 implements Bar { + baz: String + a: Int + } + type Bar_2 implements Bar { + baz: String + b: Int + } + type Bar_3 implements Bar { + baz: String + } + "#, + "schema.graphql", + ) + .unwrap(); + + let valid_schema = ValidFederationSchema::new(schema.clone()).unwrap(); + + let foo = object_field_element(&valid_schema, name!("Query"), name!("foo")); + let frag = inline_fragment_element(&valid_schema, name!("Foo"), Some(name!("Foo_1"))); + let bar = object_field_element(&valid_schema, name!("Foo_1"), name!("bar")); + let frag2 = inline_fragment_element(&valid_schema, name!("Bar"), Some(name!("Bar_1"))); + let baz = object_field_element(&valid_schema, name!("Bar_1"), name!("baz")); + + let query_root = valid_schema + .get_type(name!("Query")) + .unwrap() + .try_into() + .unwrap(); + + let path = FetchDependencyGraphNodePath::new(valid_schema, true, query_root).unwrap(); + + let path = path.add(Arc::new(foo)).unwrap(); + let path = path.add(Arc::new(frag)).unwrap(); + let path = path.add(Arc::new(bar)).unwrap(); + let path = path.add(Arc::new(frag2)).unwrap(); + let path = path.add(Arc::new(baz)).unwrap(); + + assert_eq!(".|[Foo_1]foo.bar.baz", &to_string(&path.response_path)); + } + + fn object_field_element( + schema: &ValidFederationSchema, + object: apollo_compiler::Name, + field: apollo_compiler::Name, + ) -> OpPathElement { + OpPathElement::Field(super::Field::new(super::FieldData { + schema: schema.clone(), + field_position: ObjectTypeDefinitionPosition::new(object) + .field(field) + .into(), + alias: None, + arguments: Default::default(), + directives: Default::default(), + sibling_typename: None, + })) + } + + fn interface_field_element( + schema: &ValidFederationSchema, + interface: apollo_compiler::Name, + field: apollo_compiler::Name, + ) -> OpPathElement { + OpPathElement::Field(super::Field::new(super::FieldData { + schema: schema.clone(), + field_position: InterfaceTypeDefinitionPosition::new(interface) + .field(field) + .into(), + alias: None, + arguments: Default::default(), + directives: Default::default(), + sibling_typename: None, + })) + } + + fn inline_fragment_element( + schema: &ValidFederationSchema, + parent_type_name: apollo_compiler::Name, + type_condition_name: Option, + ) -> OpPathElement { + let parent_type = schema + .get_type(parent_type_name) + .unwrap() + .try_into() + .unwrap(); + let type_condition = + type_condition_name.map(|n| schema.get_type(n).unwrap().try_into().unwrap()); + OpPathElement::InlineFragment(super::InlineFragment::new(InlineFragmentData { + schema: schema.clone(), + parent_type_position: parent_type, + type_condition_position: type_condition, + directives: Default::default(), + selection_id: SelectionId::new(), + })) + } + + fn to_string(response_path: &[FetchDataPathElement]) -> String { + format!( + ".{}", + response_path + .iter() + .map(|element| match element { + FetchDataPathElement::Key(name, conditions) => { + format!("{}{}", cond_to_string(conditions), name) + } + FetchDataPathElement::AnyIndex(conditions) => { + format!("{}{}", cond_to_string(conditions), "@") + } + FetchDataPathElement::TypenameEquals(_) => { + unimplemented!() + } + }) + .join(".") + ) + } + + fn cond_to_string(conditions: &[Name]) -> String { + if conditions.is_empty() { + return Default::default(); + } + + format!("|[{}]", conditions.iter().map(|n| n.to_string()).join(",")) + } +} diff --git a/apollo-federation/src/query_plan/mod.rs b/apollo-federation/src/query_plan/mod.rs index f620e208b3..d8c90c2c4a 100644 --- a/apollo-federation/src/query_plan/mod.rs +++ b/apollo-federation/src/query_plan/mod.rs @@ -235,11 +235,13 @@ pub struct FetchDataKeyRenamer { /// elements. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] pub enum FetchDataPathElement { - Key(Name), - AnyIndex, + Key(Name, Conditions), + AnyIndex(Conditions), TypenameEquals(Name), } +pub type Conditions = Vec; + /// Vectors of this element match a path in a query. Each element is (1) a field in a query, or (2) /// an inline fragment in a query. #[derive(Debug, Clone, PartialEq, serde::Serialize)] diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index ec338ef59d..a64f79364c 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -1,10 +1,11 @@ use std::cell::Cell; use std::num::NonZeroU32; +use std::ops::Deref; use std::sync::Arc; +use apollo_compiler::collections::HashSet; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; -use apollo_compiler::schema::ExtendedType; use apollo_compiler::validation::Valid; use apollo_compiler::ExecutableDocument; use apollo_compiler::Name; @@ -24,6 +25,7 @@ use crate::query_graph::QueryGraph; use crate::query_graph::QueryGraphNodeType; use crate::query_plan::fetch_dependency_graph::compute_nodes_for_tree; use crate::query_plan::fetch_dependency_graph::FetchDependencyGraph; +use crate::query_plan::fetch_dependency_graph::FetchDependencyGraphNodePath; use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphProcessor; use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphToCostProcessor; use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphToQueryPlanProcessor; @@ -47,7 +49,6 @@ use crate::utils::logging::snapshot; use crate::ApiSchemaOptions; use crate::Supergraph; -pub(crate) const OVERRIDE_LABEL_ARG_NAME: &str = "overrideLabel"; pub(crate) const CONTEXT_DIRECTIVE: &str = "context"; pub(crate) const JOIN_FIELD: &str = "join__field"; @@ -95,6 +96,14 @@ pub struct QueryPlannerConfig { /// in this sub-set are provided without guarantees of stability (they may be dangerous) or /// continued support (they may be removed without warning). pub debug: QueryPlannerDebugConfig, + + /// Enables type conditioned fetching. + /// This flag is a workaround, which may yield significant + /// performance degradation when computing query plans, + /// and increase query plan size. + /// + /// If you aren't aware of this flag, you probably don't need it. + pub type_conditioned_fetching: bool, } impl Default for QueryPlannerConfig { @@ -105,6 +114,7 @@ impl Default for QueryPlannerConfig { generate_query_fragments: false, incremental_delivery: Default::default(), debug: Default::default(), + type_conditioned_fetching: Default::default(), } } } @@ -186,6 +196,28 @@ impl QueryPlannerConfig { } } +#[derive(Debug, Default, Clone)] +pub struct QueryPlanOptions { + /// A set of labels which will be used _during query planning_ to + /// enable/disable edges with a matching label in their override condition. + /// Edges with override conditions require their label to be present or absent + /// from this set in order to be traversable. These labels enable the + /// progressive @override feature. + // PORT_NOTE: In JS implementation this was a Map + pub override_conditions: Vec, +} + +#[derive(Debug, Default, Clone)] +pub(crate) struct EnabledOverrideConditions(HashSet); + +impl Deref for EnabledOverrideConditions { + type Target = HashSet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + pub struct QueryPlanner { config: QueryPlannerConfig, federated_query_graph: Arc, @@ -298,11 +330,6 @@ impl QueryPlanner { .filter(|position| is_inconsistent(position.clone())) .collect::>(); - // PORT_NOTE: JS prepares a map of override conditions here, which is - // a map where the keys are all `@join__field(overrideLabel:)` argument values - // and the values are all initialised to `false`. Instead of doing that, we should - // be able to use a Set where presence means `true` and absence means `false`. - Ok(Self { config, federated_query_graph: Arc::new(query_graph), @@ -329,6 +356,7 @@ impl QueryPlanner { &self, document: &Valid, operation_name: Option, + options: QueryPlanOptions, ) -> Result { let operation = document .operations @@ -472,7 +500,9 @@ impl QueryPlanner { .clone() .into(), config: self.config.clone(), - // PORT_NOTE: JS provides `override_conditions` here: see port note in `QueryPlanner::new`. + override_conditions: EnabledOverrideConditions(HashSet::from_iter( + options.override_conditions, + )), }; let root_node = match defer_conditions { @@ -539,59 +569,6 @@ impl QueryPlanner { } fn check_unsupported_features(supergraph: &Supergraph) -> Result<(), FederationError> { - // We have a *progressive* override when `join__field` has a - // non-null value for `overrideLabel` field. - // - // This looks at object types' fields and their directive - // applications, looking specifically for `@join__field` - // arguments list. - let has_progressive_overrides = supergraph - .schema - .schema() - .types - .values() - .filter_map(|extended_type| { - // The override label args can be only on ObjectTypes - if let ExtendedType::Object(object_type) = extended_type { - Some(object_type) - } else { - None - } - }) - .flat_map(|object_type| &object_type.fields) - .flat_map(|(_, field)| { - field - .directives - .iter() - .filter(|d| d.name.as_str() == JOIN_FIELD) - }) - .any(|join_directive| { - if let Some(override_label_arg) = - join_directive.argument_by_name(OVERRIDE_LABEL_ARG_NAME) - { - // Any argument value for `overrideLabel` that's not - // null can be considered as progressive override usage - if !override_label_arg.is_null() { - return true; - } - return false; - } - false - }); - if has_progressive_overrides { - let message = "\ - `experimental_query_planner_mode: new` or `both` cannot yet \ - be used with progressive overrides. \ - Remove uses of progressive overrides to try the experimental query planner, \ - otherwise switch back to `legacy` or `both_best_effort`.\ - "; - return Err(SingleFederationError::UnsupportedFeature { - message: message.to_owned(), - kind: crate::error::UnsupportedFeatureKind::ProgressiveOverrides, - } - .into()); - } - // We will only check for `@context` direcive, since // `@fromContext` can only be used if `@context` is already // applied, and we assume a correctly composed supergraph. @@ -682,6 +659,7 @@ fn compute_root_serial_dependency_graph( operation.root_kind, &mut fetch_dependency_graph, &prev_path, + parameters.config.type_conditioned_fetching, )?; } else { // PORT_NOTE: It is unclear if they correct thing to do here is get the next ID, use @@ -719,6 +697,7 @@ pub(crate) fn compute_root_fetch_groups( root_kind: SchemaRootDefinitionKind, dependency_graph: &mut FetchDependencyGraph, path: &OpPathTree, + type_conditioned_fetching_enabled: bool, ) -> Result<(), FederationError> { // The root of the pathTree is one of the "fake" root of the subgraphs graph, // which belongs to no subgraph but points to each ones. @@ -731,7 +710,7 @@ pub(crate) fn compute_root_fetch_groups( let (_source_node, target_node) = path.graph.edge_endpoints(edge)?; let target_node = path.graph.node_weight(target_node)?; let subgraph_name = &target_node.source; - let root_type = match &target_node.type_ { + let root_type: CompositeTypeDefinitionPosition = match &target_node.type_ { QueryGraphNodeType::SchemaType(OutputTypeDefinitionPosition::Object(object)) => { object.clone().into() } @@ -741,14 +720,21 @@ pub(crate) fn compute_root_fetch_groups( ))) } }; - let fetch_dependency_node = - dependency_graph.get_or_create_root_node(subgraph_name, root_kind, root_type)?; + let fetch_dependency_node = dependency_graph.get_or_create_root_node( + subgraph_name, + root_kind, + root_type.clone(), + )?; snapshot!(dependency_graph, "tree_with_root_node"); compute_nodes_for_tree( dependency_graph, &child.tree, fetch_dependency_node, - Default::default(), + FetchDependencyGraphNodePath::new( + dependency_graph.supergraph_schema.clone(), + type_conditioned_fetching_enabled, + root_type, + )?, Default::default(), &Default::default(), )?; @@ -1083,7 +1069,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "accounts") { @@ -1115,7 +1103,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Sequence { @@ -1204,7 +1194,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Parallel { @@ -1315,7 +1307,9 @@ type User let mut config = QueryPlannerConfig::default(); config.debug.bypass_planner_for_single_subgraph = true; let planner = QueryPlanner::new(&supergraph, config).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "A") { @@ -1359,7 +1353,9 @@ type User .unwrap(); let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "accounts") { @@ -1418,7 +1414,9 @@ type User .unwrap(); let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "accounts") { @@ -1478,7 +1476,9 @@ type User .unwrap(); let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); // Make sure `fragment F2` contains `...F1`. insta::assert_snapshot!(plan, @r###" QueryPlan { @@ -1535,7 +1535,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "Subgraph1") { diff --git a/apollo-federation/src/query_plan/query_planning_traversal.rs b/apollo-federation/src/query_plan/query_planning_traversal.rs index 2dab41debf..cb5e6bb9f1 100644 --- a/apollo-federation/src/query_plan/query_planning_traversal.rs +++ b/apollo-federation/src/query_plan/query_planning_traversal.rs @@ -30,11 +30,13 @@ use crate::query_graph::QueryGraph; use crate::query_graph::QueryGraphNodeType; use crate::query_plan::fetch_dependency_graph::compute_nodes_for_tree; use crate::query_plan::fetch_dependency_graph::FetchDependencyGraph; +use crate::query_plan::fetch_dependency_graph::FetchDependencyGraphNodePath; use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphProcessor; use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphToCostProcessor; use crate::query_plan::generate::generate_all_plans_and_find_best; use crate::query_plan::generate::PlanBuilder; use crate::query_plan::query_planner::compute_root_fetch_groups; +use crate::query_plan::query_planner::EnabledOverrideConditions; use crate::query_plan::query_planner::QueryPlannerConfig; use crate::query_plan::query_planner::QueryPlanningStatistics; use crate::query_plan::QueryPlanCost; @@ -71,6 +73,7 @@ pub(crate) struct QueryPlanningParameters<'a> { /// The configuration for the query planner. pub(crate) config: QueryPlannerConfig, pub(crate) statistics: &'a QueryPlanningStatistics, + pub(crate) override_conditions: EnabledOverrideConditions, } pub(crate) struct QueryPlanningTraversal<'a, 'b> { @@ -246,6 +249,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { &mut traversal, excluded_destinations, excluded_conditions, + ¶meters.override_conditions, )?; traversal.open_branches = map_options_to_selections(selection_set, initial_options); @@ -336,6 +340,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { self.parameters.supergraph_schema.clone(), &operation_element, /*resolver*/ self, + &self.parameters.override_conditions, )?; let Some(followups_for_option) = followups_for_option else { // There is no valid way to advance the current operation element from this option @@ -403,7 +408,9 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { let mut new_simultaneous_paths = vec![]; for simultaneous_path in &option.paths.0 { new_simultaneous_paths.push(Arc::new( - simultaneous_path.terminate_with_non_requested_typename_field()?, + simultaneous_path.terminate_with_non_requested_typename_field( + &self.parameters.override_conditions, + )?, )); } closed_paths.push(Arc::new(ClosedPath { @@ -663,7 +670,11 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { *root, &single_choice_branches, )?; - self.updated_dependency_graph(&mut initial_dependency_graph, &initial_tree)?; + self.updated_dependency_graph( + &mut initial_dependency_graph, + &initial_tree, + self.parameters.config.type_conditioned_fetching, + )?; snapshot!( initial_dependency_graph, "Updated dep graph with initial tree" @@ -963,34 +974,44 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { &self, dependency_graph: &mut FetchDependencyGraph, path_tree: &OpPathTree, + type_conditioned_fetching_enabled: bool, ) -> Result<(), FederationError> { let is_root_path_tree = matches!( path_tree.graph.node_weight(path_tree.node)?.type_, QueryGraphNodeType::FederatedRootType(_) ); if is_root_path_tree { - compute_root_fetch_groups(self.root_kind, dependency_graph, path_tree)?; + compute_root_fetch_groups( + self.root_kind, + dependency_graph, + path_tree, + type_conditioned_fetching_enabled, + )?; } else { let query_graph_node = path_tree.graph.node_weight(path_tree.node)?; let subgraph_name = &query_graph_node.source; - let root_type = match &query_graph_node.type_ { + let root_type: CompositeTypeDefinitionPosition = match &query_graph_node.type_ { QueryGraphNodeType::SchemaType(position) => position.clone().try_into()?, QueryGraphNodeType::FederatedRootType(_) => { return Err(FederationError::internal( "unexpected FederatedRootType not at the start of an OpPathTree", - )) + )); } }; let fetch_dependency_node = dependency_graph.get_or_create_root_node( subgraph_name, self.root_kind, - root_type, + root_type.clone(), )?; compute_nodes_for_tree( dependency_graph, path_tree, fetch_dependency_node, - Default::default(), + FetchDependencyGraphNodePath::new( + dependency_graph.supergraph_schema.clone(), + self.parameters.config.type_conditioned_fetching, + root_type, + )?, Default::default(), &Default::default(), )?; @@ -1039,6 +1060,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { .clone(), config: self.parameters.config.clone(), statistics: self.parameters.statistics, + override_conditions: self.parameters.override_conditions.clone(), }; let best_plan_opt = QueryPlanningTraversal::new_inner( ¶meters, @@ -1069,11 +1091,15 @@ impl<'a: 'b, 'b> PlanBuilder> for QueryPlanningTravers tree: Arc, ) -> Result { let mut updated_graph = plan_info.fetch_dependency_graph.clone(); - self.updated_dependency_graph(&mut updated_graph, &tree) - .map(|_| PlanInfo { - fetch_dependency_graph: updated_graph, - path_tree: plan_info.path_tree.merge(&tree), - }) + self.updated_dependency_graph( + &mut updated_graph, + &tree, + self.parameters.config.type_conditioned_fetching, + ) + .map(|_| PlanInfo { + fetch_dependency_graph: updated_graph, + path_tree: plan_info.path_tree.merge(&tree), + }) } fn compute_plan_cost( diff --git a/apollo-federation/src/utils/fallible_iterator.rs b/apollo-federation/src/utils/fallible_iterator.rs index 83564c74f6..36dc23b845 100644 --- a/apollo-federation/src/utils/fallible_iterator.rs +++ b/apollo-federation/src/utils/fallible_iterator.rs @@ -77,9 +77,7 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// the iterator will yield the `Err` in its place. Lastly, if the predicate yields `Ok(true)`, /// the iterator will yield `Ok(val)`. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// + /// ```ignore /// // A totally accurate prime checker /// fn is_prime(i: usize) -> Result { /// match i { @@ -113,9 +111,7 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// method is very similar to `Itertools::filter_ok` except the predicate for this method is /// fallible. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// + /// ```ignore /// // A totally accurate prime checker /// fn is_prime(i: usize) -> Result { /// match i { @@ -147,9 +143,7 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// `Ok(false)`, the returned value will be `Ok(false)`. If that item is `Err`, than that `Err` /// is returned. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// + /// ```ignore /// // A totally accurate prime checker /// fn is_prime(i: usize) -> Result { /// match i { @@ -188,19 +182,11 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// is `Ok`, it is given to the predicate. If the predicate returns `false`, this method /// returns `Ok(false)`. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// - /// type Item = Result; - /// - /// fn is_even(i: usize) -> bool { - /// i % 2 == 0 - /// } - /// - /// let first_values: Vec = vec![]; - /// let second_values: Vec = vec![Ok(1), Err(())]; - /// let third_values: Vec = vec![Ok(0), Ok(1), Ok(2)]; - /// let fourth_values: Vec = vec![Err(()), Ok(0)]; + /// ```ignore + /// let first_values = vec![]; + /// let second_values = vec![Ok(1), Err(())]; + /// let third_values = vec![Ok(0), Ok(1), Ok(2)]; + /// let fourth_values = vec![Err(()), Ok(0)]; /// /// assert_eq!(Ok(true), first_values.into_iter().ok_and_all(is_even)); /// assert_eq!(Ok(false), second_values.into_iter().ok_and_all(is_even)); @@ -224,11 +210,7 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// predicate returns `Err`, that `Err` is returned. If the predicate returns `Ok(false)`, /// `Ok(false)` is returned. By default, this function returned `Ok(true)`. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// - /// type Item = Result; - /// + /// ```ignore /// // A totally accurate prime checker /// fn is_prime(i: usize) -> Result { /// match i { @@ -238,12 +220,12 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// } /// } /// - /// let first_values: Vec = vec![]; - /// let second_values: Vec = vec![Ok(0), Err(())]; - /// let third_values: Vec = vec![Ok(2), Ok(3)]; - /// let fourth_values: Vec = vec![Err(()), Ok(2)]; - /// let fifth_values: Vec = vec![Ok(2), Err(())]; - /// let sixth_values: Vec = vec![Ok(4), Ok(3)]; + /// let first_values = vec![]; + /// let second_values = vec![Ok(0), Err(())]; + /// let third_values = vec![Ok(2), Ok(3)]; + /// let fourth_values = vec![Err(()), Ok(2)]; + /// let fifth_values = vec![Ok(2), Err(())]; + /// let sixth_values = vec![Ok(4), Ok(3)]; /// /// assert_eq!(Ok(true), first_values.into_iter().and_then_all(is_prime)); /// assert_eq!(Err(()), second_values.into_iter().and_then_all(is_prime)); @@ -275,9 +257,7 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// `Ok(true)`, the returned value will be `Ok(true)`. If that item is `Err`, than that `Err` /// is returned. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// + /// ```ignore /// // A totally accurate prime checker /// fn is_prime(i: usize) -> Result { /// match i { @@ -316,19 +296,11 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// is `Ok`, it is given to the predicate. If the predicate returns `true`, this method returns /// `Ok(true)`. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// - /// type Item = Result; - /// - /// fn is_even(i: usize) -> bool { - /// i % 2 == 0 - /// } - /// - /// let first_values: Vec = vec![]; - /// let second_values: Vec = vec![Ok(0), Err(())]; - /// let third_values: Vec = vec![Ok(1), Ok(3)]; - /// let fourth_values: Vec = vec![Err(()), Ok(0)]; + /// ```ignore + /// let first_values = vec![]; + /// let second_values = vec![Ok(0), Err(())]; + /// let third_values = vec![Ok(1), Ok(3)]; + /// let fourth_values = vec![Err(()), Ok(0)]; /// /// assert_eq!(Ok(false), first_values.into_iter().ok_and_any(is_even)); /// assert_eq!(Ok(true), second_values.into_iter().ok_and_any(is_even)); @@ -352,11 +324,7 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// predicate returns `Err`, that `Err` is returned. If the predicate returns `Ok(true)`, /// `Ok(true)` is returned. By default, this function returned `Ok(false)`. /// - /// ```rust - /// use apollo_federation::utils::FallibleIterator; - /// - /// type Item = Result; - /// + /// ```ignore /// // A totally accurate prime checker /// fn is_prime(i: usize) -> Result { /// match i { @@ -366,12 +334,12 @@ pub(crate) trait FallibleIterator: Sized + Itertools { /// } /// } /// - /// let first_values: Vec = vec![]; - /// let second_values: Vec = vec![Ok(0), Err(())]; - /// let third_values: Vec = vec![Ok(3), Ok(4)]; - /// let fourth_values: Vec = vec![Err(()), Ok(2)]; - /// let fifth_values: Vec = vec![Ok(2), Err(())]; - /// let sixth_values: Vec = vec![Ok(4), Ok(5)]; + /// let first_values = vec![]; + /// let second_values = vec![Ok(0), Err(())]; + /// let third_values = vec![Ok(3), Ok(4)]; + /// let fourth_values = vec![Err(()), Ok(2)]; + /// let fifth_values = vec![Ok(2), Err(())]; + /// let sixth_values = vec![Ok(4), Ok(5)]; /// /// assert_eq!(Ok(false), first_values.into_iter().and_then_any(is_prime)); /// assert_eq!(Err(()), second_values.into_iter().and_then_any(is_prime)); @@ -527,3 +495,107 @@ where self.iter.next().map(|res| res.or_else(&mut self.map)) } } + +// Ideally, these would be doc tests, but gating access to the `utils` module is messy. +#[cfg(test)] +mod tests { + use super::*; + + type Item = Result; + + fn is_prime(i: usize) -> Result { + match i { + 0 | 1 => Err(()), // 0 and 1 are neither prime or composite + 2 | 3 => Ok(true), + _ => Ok(false), // Every other number is composite, I guess + } + } + + fn is_even(i: usize) -> bool { + i % 2 == 0 + } + + fn test_fallible_filter() { + let vals = (1..6).fallible_filter(|i| is_prime(*i)); + itertools::assert_equal(vals, vec![Err(()), Ok(2), Ok(3)]); + } + + fn test_and_then_filter() { + let vals = vec![Ok(0), Err(()), Err(()), Ok(3), Ok(4)] + .into_iter() + .and_then_filter(|i| is_prime(*i)); + itertools::assert_equal(vals, vec![Err(()), Err(()), Err(()), Ok(3)]); + } + + fn test_fallible_all() { + assert_eq!(Ok(true), [].into_iter().fallible_all(is_prime)); + assert_eq!(Ok(true), (2..4).fallible_all(is_prime)); + assert_eq!(Err(()), (1..4).fallible_all(is_prime)); + assert_eq!(Ok(false), (2..5).fallible_all(is_prime)); + assert_eq!(Err(()), (1..5).fallible_all(is_prime)); + } + + fn test_ok_and_all() { + let first_values: Vec = vec![]; + let second_values: Vec = vec![Ok(1), Err(())]; + let third_values: Vec = vec![Ok(0), Ok(1), Ok(2)]; + let fourth_values: Vec = vec![Err(()), Ok(0)]; + + assert_eq!(Ok(true), first_values.into_iter().ok_and_all(is_even)); + assert_eq!(Ok(false), second_values.into_iter().ok_and_all(is_even)); + assert_eq!(Ok(false), third_values.into_iter().ok_and_all(is_even)); + assert_eq!(Err(()), fourth_values.into_iter().ok_and_all(is_even)); + } + + fn test_and_then_all() { + let first_values: Vec = vec![]; + let second_values: Vec = vec![Ok(0), Err(())]; + let third_values: Vec = vec![Ok(2), Ok(3)]; + let fourth_values: Vec = vec![Err(()), Ok(2)]; + let fifth_values: Vec = vec![Ok(2), Err(())]; + let sixth_values: Vec = vec![Ok(4), Ok(3)]; + + assert_eq!(Ok(true), first_values.into_iter().and_then_all(is_prime)); + assert_eq!(Err(()), second_values.into_iter().and_then_all(is_prime)); + assert_eq!(Ok(true), third_values.into_iter().and_then_all(is_prime)); + assert_eq!(Err(()), fourth_values.into_iter().and_then_all(is_prime)); + assert_eq!(Err(()), fifth_values.into_iter().and_then_all(is_prime)); + assert_eq!(Ok(false), sixth_values.into_iter().and_then_all(is_prime)); + } + + fn test_fallible_any() { + assert_eq!(Ok(false), [].into_iter().fallible_any(is_prime)); + assert_eq!(Ok(true), (2..5).fallible_any(is_prime)); + assert_eq!(Ok(false), (4..5).fallible_any(is_prime)); + assert_eq!(Err(()), (1..4).fallible_any(is_prime)); + assert_eq!(Err(()), (1..5).fallible_any(is_prime)); + } + + fn test_ok_and_any() { + let first_values: Vec = vec![]; + let second_values: Vec = vec![Ok(0), Err(())]; + let third_values: Vec = vec![Ok(1), Ok(3)]; + let fourth_values: Vec = vec![Err(()), Ok(0)]; + + assert_eq!(Ok(false), first_values.into_iter().ok_and_any(is_even)); + assert_eq!(Ok(true), second_values.into_iter().ok_and_any(is_even)); + assert_eq!(Ok(false), third_values.into_iter().ok_and_any(is_even)); + assert_eq!(Err(()), fourth_values.into_iter().ok_and_any(is_even)); + } + + fn test_and_then_any() { + let first_values: Vec = vec![]; + let second_values: Vec = vec![Ok(0), Err(())]; + let third_values: Vec = vec![Ok(3), Ok(4)]; + let fourth_values: Vec = vec![Err(()), Ok(2)]; + let fifth_values: Vec = vec![Ok(2), Err(())]; + let sixth_values: Vec = vec![Ok(4), Ok(5)]; + + assert_eq!(Ok(false), first_values.into_iter().and_then_any(is_prime)); + assert_eq!(Err(()), second_values.into_iter().and_then_any(is_prime)); + assert_eq!(Ok(true), third_values.into_iter().and_then_any(is_prime)); + assert_eq!(Err(()), fourth_values.into_iter().and_then_any(is_prime)); + assert_eq!(Ok(true), fifth_values.into_iter().and_then_any(is_prime)); + assert_eq!(Ok(false), sixth_values.into_iter().and_then_any(is_prime)); + } +} diff --git a/apollo-federation/src/utils/mod.rs b/apollo-federation/src/utils/mod.rs index ec910a87f8..fe4b815704 100644 --- a/apollo-federation/src/utils/mod.rs +++ b/apollo-federation/src/utils/mod.rs @@ -2,4 +2,5 @@ mod fallible_iterator; pub mod logging; + pub(crate) use fallible_iterator::*; diff --git a/apollo-federation/tests/query_plan/build_query_plan_support.rs b/apollo-federation/tests/query_plan/build_query_plan_support.rs index ed70798f2f..b73596590b 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_support.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_support.rs @@ -14,8 +14,7 @@ use sha1::Digest; const ROVER_FEDERATION_VERSION: &str = "2.7.4"; -// TODO: use 2.7 when join v0.4 is fully supported in this crate -const IMPLICIT_LINK_DIRECTIVE: &str = r#"@link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])"#; +const DEFAULT_LINK_DIRECTIVE: &str = r#"@link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])"#; /// Runs composition on the given subgraph schemas and return `(api_schema, query_planner)` /// @@ -60,6 +59,18 @@ macro_rules! subgraph_name { /// formatted query plan string. /// Run `cargo insta review` to diff and accept changes to the generated query plan. macro_rules! assert_plan { + ($api_schema_and_planner: expr, $operation: expr, $options: expr, @$expected: literal) => {{ + let (api_schema, planner) = $api_schema_and_planner; + let document = apollo_compiler::ExecutableDocument::parse_and_validate( + api_schema.schema(), + $operation, + "operation.graphql", + ) + .unwrap(); + let plan = planner.build_query_plan(&document, None, $options).unwrap(); + insta::assert_snapshot!(plan, @$expected); + plan + }}; ($api_schema_and_planner: expr, $operation: expr, @$expected: literal) => {{ let (api_schema, planner) = $api_schema_and_planner; let document = apollo_compiler::ExecutableDocument::parse_and_validate( @@ -68,7 +79,7 @@ macro_rules! assert_plan { "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner.build_query_plan(&document, None, Default::default()).unwrap(); insta::assert_snapshot!(plan, @$expected); plan }}; @@ -110,7 +121,7 @@ pub(crate) fn compose( .map(|(name, schema)| { ( *name, - format!("extend schema {IMPLICIT_LINK_DIRECTIVE}\n\n{}", schema,), + format!("extend schema {DEFAULT_LINK_DIRECTIVE}\n\n{}", schema,), ) }) .collect(); diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests.rs b/apollo-federation/tests/query_plan/build_query_plan_tests.rs index 8f256843e2..acef092c2c 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests.rs @@ -44,11 +44,11 @@ mod merged_abstract_types_handling; mod mutations; mod named_fragments; mod named_fragments_preservation; +mod overrides; mod provides; mod requires; mod shareable_root_fields; mod subscriptions; - // TODO: port the rest of query-planner-js/src/__tests__/buildPlan.test.ts #[test] @@ -474,10 +474,8 @@ fn it_executes_mutation_operations_in_sequence() { ); } -/// @requires references external field indirectly { +/// @requires references external field indirectly #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure (appears to be visiting wrong subgraph) fn key_where_at_external_is_not_at_top_level_of_selection_of_requires() { // Field issue where we were seeing a FetchGroup created where the fields used by the key to jump subgraphs // were not properly fetched. In the below test, this test will ensure that 'k2' is properly collected @@ -1079,3 +1077,219 @@ fn test_merging_fetches_reset_cached_costs() { "### ); } + +#[test] +fn handles_multiple_conditions_on_abstract_types() { + let planner = planner!( + books: r#" + type Book @key(fields: "id") { + id: ID! + title: String + } + "#, + magazines: r#" + type Magazine @key(fields: "id") { + id: ID! + title: String + } + "#, + products: r#" + type Query { + products: [Product] + } + + interface Product { + id: ID! + sku: String + dimensions: ProductDimension + } + + type ProductDimension @shareable { + size: String + weight: Float + } + + type Book implements Product @key(fields: "id") { + id: ID! + sku: String + dimensions: ProductDimension @shareable + } + + type Magazine implements Product @key(fields: "id") { + id: ID! + sku: String + dimensions: ProductDimension @shareable + } + "#, + reviews: r#" + type Book implements Product @key(fields: "id") { + id: ID! + reviews: [Review!]! + } + + type Magazine implements Product @key(fields: "id") { + id: ID! + reviews: [Review!]! + } + + interface Product { + id: ID! + reviews: [Review!]! + } + + type Review { + id: Int! + body: String! + product: Product + } + "#, + ); + + assert_plan!( + &planner, + r#" + query ($title: Boolean = true) { + products { + id + reviews { + product { + id + ... on Book @include(if: $title) { + title + ... on Book @skip(if: $title) { + sku + } + } + ... on Magazine { + sku + } + } + } + } + } + "#, + @r###" + QueryPlan { + Sequence { + Fetch(service: "products") { + { + products { + __typename + id + ... on Book { + __typename + id + } + ... on Magazine { + __typename + id + } + } + } + }, + Flatten(path: "products.@") { + Fetch(service: "reviews") { + { + ... on Book { + __typename + id + } + ... on Magazine { + __typename + id + } + } => + { + ... on Book { + reviews { + product { + __typename + id + ... on Book @include(if: $title) { + __typename + id + ... on Book @skip(if: $title) { + __typename + id + } + } + ... on Magazine { + __typename + id + } + } + } + } + ... on Magazine { + reviews { + product { + __typename + id + ... on Book @include(if: $title) { + __typename + id + ... on Book @skip(if: $title) { + __typename + id + } + } + ... on Magazine { + __typename + id + } + } + } + } + } + }, + }, + Parallel { + Flatten(path: "products.@.reviews.@.product") { + Fetch(service: "products") { + { + ... on Book { + ... on Book { + __typename + id + } + } + ... on Magazine { + __typename + id + } + } => + { + ... on Book @skip(if: $title) { + ... on Book @include(if: $title) { + sku + } + } + ... on Magazine { + sku + } + } + }, + }, + Include(if: $title) { + Flatten(path: "products.@.reviews.@.product") { + Fetch(service: "books") { + { + ... on Book { + __typename + id + } + } => + { + ... on Book { + title + } + } + }, + }, + }, + }, + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs index a18565aed0..884c10e7e1 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs @@ -253,7 +253,9 @@ fn correctly_handle_case_where_there_is_too_many_plans_to_consider() { "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); // Note: The way the code that handle multiple plans currently work, it mess up the order of fields a bit. It's not a // big deal in practice cause everything gets re-order in practice during actual execution, but this means it's a tad diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs index 5a99022d4a..0cd50ce299 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/interface_object.rs @@ -40,8 +40,6 @@ const SUBGRAPH2: &str = r#" "#; #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure (fetch node for `iFromS1.y` is missing) fn can_use_a_key_on_an_interface_object_type() { let planner = planner!( S1: SUBGRAPH1, @@ -223,8 +221,6 @@ fn does_not_rely_on_an_interface_object_directly_for_typename() { } #[test] -#[should_panic(expected = r#"snapshot assertion"#)] -// TODO: investigate this failure (missing fetch node for `iFromS2 { ... on I { y } }`) fn does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is_requested() { let planner = planner!( S1: SUBGRAPH1, @@ -294,8 +290,6 @@ fn does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is } #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure (fetch node for `iFromS1.y` is missing) fn can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation() { let planner = planner!( S1: SUBGRAPH1, @@ -354,13 +348,19 @@ fn can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation( let rewrite = rewrites[0].clone(); match rewrite.deref() { FetchDataRewrite::ValueSetter(v) => { - assert_eq!(v.path.len(), 1); + assert_eq!(v.path.len(), 2); match &v.path[0] { FetchDataPathElement::TypenameEquals(typename) => { assert_eq!(*typename, apollo_compiler::name!("A")) } _ => unreachable!("Expected FetchDataPathElement::TypenameEquals path"), } + match &v.path[1] { + FetchDataPathElement::Key(name, _conditions) => { + assert_eq!(*name, apollo_compiler::name!("__typename")) + } + _ => unreachable!("Expected FetchDataPathElement::Key path"), + } assert_eq!(v.set_value_to, "I"); } _ => unreachable!("Expected FetchDataRewrite::ValueSetter rewrite"), @@ -423,8 +423,6 @@ fn handles_query_of_an_interface_field_for_a_specific_implementation_when_query_ } #[test] -#[should_panic(expected = r#"snapshot assertion"#)] -// TODO: investigate this failure (missing fetch node for "everything.@ { ... on I { expansiveField } }") fn it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_with_lists() { let planner = planner!( S1: r#" @@ -517,8 +515,6 @@ fn it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_wit } #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure (missing fetch nodes for i.x and i.y) fn it_handles_requires_on_concrete_type_of_field_provided_by_interface_object() { let planner = planner!( S1: r#" @@ -613,8 +609,6 @@ fn it_handles_requires_on_concrete_type_of_field_provided_by_interface_object() } #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure (missing fetch node for `i.t.relatedIs.id`) fn it_handles_interface_object_in_nested_entity() { let planner = planner!( S1: r#" @@ -716,8 +710,6 @@ fn it_handles_interface_object_in_nested_entity() { } #[test] -#[should_panic(expected = "snapshot assertion")] -// TODO: investigate this failure fn it_handles_interface_object_input_rewrites_when_cloning_dependency_graph() { let planner = planner!( S1: r#" @@ -797,33 +789,33 @@ fn it_handles_interface_object_input_rewrites_when_cloning_dependency_graph() { } }, Parallel { - Flatten(path: "i") { - Fetch(service: "S2") { + Flatten(path: "i.i2") { + Fetch(service: "S4") { { - ... on I { + ... on T { __typename - i1 + t1 } } => { - ... on I { - i3 + ... on T { + __typename + t2 } } }, }, - Flatten(path: "i.i2") { - Fetch(service: "S3") { + Flatten(path: "i") { + Fetch(service: "S2") { { - ... on T { + ... on I { __typename - t1 + i1 } } => { - ... on T { - __typename - t2 + ... on I { + i3 } } }, diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/overrides.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides.rs new file mode 100644 index 0000000000..c0f1b91a31 --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides.rs @@ -0,0 +1,375 @@ +use apollo_federation::query_plan::query_planner::QueryPlanOptions; + +mod shareable; + +#[test] +fn it_handles_progressive_override_on_root_fields() { + let planner = planner!( + s1: r#" + type Query { + hello: String + } + "#, + s2: r#" + type Query { + hello: String @override(from: "s1", label: "test") + } + "#, + ); + assert_plan!( + &planner, + r#" + { + hello + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Fetch(service: "s2") { + { + hello + } + }, + } + "### + ); +} + +#[test] +fn it_does_not_override_unset_labels_on_root_fields() { + let planner = planner!( + s1: r#" + type Query { + hello: String + } + "#, + s2: r#" + type Query { + hello: String @override(from: "s1", label: "test") + } + "#, + ); + assert_plan!( + &planner, + r#" + { + hello + } + "#, + + @r###" + QueryPlan { + Fetch(service: "s1") { + { + hello + } + }, + } + "### + ); +} + +#[test] +fn it_handles_progressive_override_on_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "s2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f1 + f2 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_does_not_override_unset_labels_on_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + + @r###" + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + t { + __typename + id + f1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "s2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f2 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_handles_progressive_override_on_nested_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t2 { + t { + f1 + } + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + t2 { + t { + __typename + id + } + } + } + }, + Flatten(path: "t2.t") { + Fetch(service: "s2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f1 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_does_not_override_unset_labels_on_nested_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t2 { + t { + f1 + } + } + } + "#, + + @r###" + QueryPlan { + Fetch(service: "s1") { + { + t2 { + t { + f1 + } + } + } + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/overrides/shareable.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides/shareable.rs new file mode 100644 index 0000000000..103017912e --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides/shareable.rs @@ -0,0 +1,244 @@ +use apollo_federation::query_plan::query_planner::QueryPlanOptions; + +const S1: &str = r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + f1: String @shareable + } +"#; + +const S2: &str = r#" + type T @key(fields: "id") { + id: ID! + f1: String @shareable @override(from: "S1", label: "test") + f2: String + } +"#; + +const S3: &str = r#" + type T @key(fields: "id") { + id: ID! + f1: String @shareable + f3: String + } +"#; + +#[test] +fn it_overrides_to_s2_when_label_is_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "S2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f2 + f1 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_resolves_in_s1_when_label_is_not_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + f1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "S2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f2 + } + } + }, + }, + }, + } + "### + ); +} + +// This is very similar to the S2 example. The fact that the @override in S2 +// specifies _from_ S1 actually affects all T.f1 fields the same way (except +// S1). That is to say, it's functionally equivalent to have the `@override` +// exist in either S2 or S3 from S2/S3/Sn's perspective. It's helpful to +// test here that the QP will take a path through _either_ S2 or S3 when +// appropriate to do so. In these tests and the previous S2 tests, +// "appropriate" is determined by the other fields being selected in the +// query. +#[test] +fn it_overrides_f1_to_s3_when_label_is_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f3 + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "S3") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f3 + f1 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_resolves_f1_in_s1_when_label_is_not_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f3 + } + } + "#, + + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + f1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "S3") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f3 + } + } + }, + }, + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/supergraphs/add_back_sibling_typename_to_interface_object.graphql b/apollo-federation/tests/query_plan/supergraphs/add_back_sibling_typename_to_interface_object.graphql index 5263932418..08d3c44ea2 100644 --- a/apollo-federation/tests/query_plan/supergraphs/add_back_sibling_typename_to_interface_object.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/add_back_sibling_typename_to_interface_object.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: f041374db74b06004feb7864eece06a163328b37 +# Composed from subgraphs with hash: 9c69e32dbbd50fb46ea218c3581b08f4f084b9b9 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -44,6 +46,8 @@ interface Item name: String! @join__field(graph: SUBGRAPH1) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/adjacent_mutations_get_merged.graphql b/apollo-federation/tests/query_plan/supergraphs/adjacent_mutations_get_merged.graphql index e99930397b..ae84cc8ed8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/adjacent_mutations_get_merged.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/adjacent_mutations_get_merged.graphql @@ -1,15 +1,17 @@ -# Composed from subgraphs with hash: 2655e7da6754e73955fece01d7cbb5f21085bdbb +# Composed from subgraphs with hash: 54adb76945715a2ce0e068d2635d71ca4166b289 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query mutation: Mutation } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -30,6 +32,8 @@ type Foo baz: Int @join__field(graph: SUBGRAPHB) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/allows_setting_down_to_1.graphql b/apollo-federation/tests/query_plan/supergraphs/allows_setting_down_to_1.graphql index 0b6baf2e68..42f3b4b125 100644 --- a/apollo-federation/tests/query_plan/supergraphs/allows_setting_down_to_1.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/allows_setting_down_to_1.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: bf831e2e6890f60e5c8e93bc52ce549323cb23e8 +# Composed from subgraphs with hash: 91d4d0661d413b60ae2ed463c074c4180d7b849e schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/another_mix_of_fragments_indirection_and_unions.graphql b/apollo-federation/tests/query_plan/supergraphs/another_mix_of_fragments_indirection_and_unions.graphql index cafda1ceee..9b6bb51265 100644 --- a/apollo-federation/tests/query_plan/supergraphs/another_mix_of_fragments_indirection_and_unions.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/another_mix_of_fragments_indirection_and_unions.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 025701346c4869b221f7700ee29514f70ca1bb6c +# Composed from subgraphs with hash: b0bcf31c6c8c64e53e4286dbf397738e41ecbda7 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ interface I id2: ID! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/avoid_considering_indirect_paths_from_the_root_when_a_more_direct_one_exists.graphql b/apollo-federation/tests/query_plan/supergraphs/avoid_considering_indirect_paths_from_the_root_when_a_more_direct_one_exists.graphql index 2a7e9c07f9..b5d2335e2c 100644 --- a/apollo-federation/tests/query_plan/supergraphs/avoid_considering_indirect_paths_from_the_root_when_a_more_direct_one_exists.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/avoid_considering_indirect_paths_from_the_root_when_a_more_direct_one_exists.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 995342f0aeb7c35ebe233102083b817ae5d9b0a8 +# Composed from subgraphs with hash: 9df3e1f539626e2d33ad1aeab8fa51e27fd1f441 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/avoids_unnecessary_fetches.graphql b/apollo-federation/tests/query_plan/supergraphs/avoids_unnecessary_fetches.graphql index ebd8d020f0..7ea5c3595d 100644 --- a/apollo-federation/tests/query_plan/supergraphs/avoids_unnecessary_fetches.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/avoids_unnecessary_fetches.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 1fd25bcad80101354eb093f4847039ee2ef80241 +# Composed from subgraphs with hash: 8ba19e06c01ab40b92eda93ac77de08c57856299 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -30,6 +32,8 @@ type A idA1: ID! @join__field(graph: SUBGRAPH3) @join__field(graph: SUBGRAPH4) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/basic_subscription_query_plan.graphql b/apollo-federation/tests/query_plan/supergraphs/basic_subscription_query_plan.graphql index 3af33311f3..8b559e6727 100644 --- a/apollo-federation/tests/query_plan/supergraphs/basic_subscription_query_plan.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/basic_subscription_query_plan.graphql @@ -1,15 +1,17 @@ -# Composed from subgraphs with hash: 8a2b0d4121c001b0263da2dd5f1a7f77801c3a80 +# Composed from subgraphs with hash: f7613316e8b925d2e1fb7f78b351920adfa4a595 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query subscription: Subscription } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -21,6 +23,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/basic_subscription_with_single_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/basic_subscription_with_single_subgraph.graphql index 4bcaf7ed8f..e917252972 100644 --- a/apollo-federation/tests/query_plan/supergraphs/basic_subscription_with_single_subgraph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/basic_subscription_with_single_subgraph.graphql @@ -1,15 +1,17 @@ -# Composed from subgraphs with hash: e6e4f1ffd5eae3125cc831f8eb1e46200243da74 +# Composed from subgraphs with hash: 409fcb3d17fb926642cf9483b5c1db292c219eb1 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query subscription: Subscription } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -21,6 +23,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/can_be_set_to_an_arbitrary_number.graphql b/apollo-federation/tests/query_plan/supergraphs/can_be_set_to_an_arbitrary_number.graphql index 0b6baf2e68..42f3b4b125 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_be_set_to_an_arbitrary_number.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_be_set_to_an_arbitrary_number.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: bf831e2e6890f60e5c8e93bc52ce549323cb23e8 +# Composed from subgraphs with hash: 91d4d0661d413b60ae2ed463c074c4180d7b849e schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_entity_fetch.graphql b/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_entity_fetch.graphql index f470a875f3..077541aced 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_entity_fetch.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_entity_fetch.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 69f84bc4036179c07a518c8c9b871bd0850f1606 +# Composed from subgraphs with hash: 24f37faeded16eb10b0ffc6c3c2a0e936f406801 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_root_fetch.graphql b/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_root_fetch.graphql index 55a80e69c6..4d21c8ec70 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_root_fetch.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_reuse_fragments_in_subgraph_where_they_only_partially_apply_in_root_fetch.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: fdeaf767c1e72134946220497d72c7db3cebbe19 +# Composed from subgraphs with hash: d4c3168300df54a934d76deb9884e98ba647a676 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_from_an_interface_object_type.graphql b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_from_an_interface_object_type.graphql index 14f041319c..97189f9365 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_from_an_interface_object_type.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_from_an_interface_object_type.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 43bf934e8826f0b4d17867095ea2025fb352287a +# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I y: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type.graphql b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type.graphql index 14f041319c..97189f9365 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 43bf934e8826f0b4d17867095ea2025fb352287a +# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I y: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation.graphql b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation.graphql index 14f041319c..97189f9365 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_use_a_key_on_an_interface_object_type_even_for_a_concrete_implementation.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 43bf934e8826f0b4d17867095ea2025fb352287a +# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I y: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/can_use_same_root_operation_from_multiple_subgraphs_in_parallel.graphql b/apollo-federation/tests/query_plan/supergraphs/can_use_same_root_operation_from_multiple_subgraphs_in_parallel.graphql index 66e59ae104..843563aa3f 100644 --- a/apollo-federation/tests/query_plan/supergraphs/can_use_same_root_operation_from_multiple_subgraphs_in_parallel.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/can_use_same_root_operation_from_multiple_subgraphs_in_parallel.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: ef972ffb2fc3cdf19f816cdc10e29756fb3f0656 +# Composed from subgraphs with hash: 34751a1a79473dcec3aad139f107e5b73a855e0d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/correctly_generate_plan_built_from_some_non_individually_optimal_branch_options.graphql b/apollo-federation/tests/query_plan/supergraphs/correctly_generate_plan_built_from_some_non_individually_optimal_branch_options.graphql index 2766e3b307..56e9fef51e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/correctly_generate_plan_built_from_some_non_individually_optimal_branch_options.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/correctly_generate_plan_built_from_some_non_individually_optimal_branch_options.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 38b15e780cba3d9d7cb6288e027386e3d612102a +# Composed from subgraphs with hash: 6ca9a3e7d089605b227c291630c3f089dde1f38d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql b/apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql index cfa0ce48ca..80f221ab6f 100644 --- a/apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/correctly_handle_case_where_there_is_too_many_plans_to_consider.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 569b27fb405f035347590f869510e5cc78058214 +# Composed from subgraphs with hash: 8a8e74cff42f95e71955aa96d62ef9f9589739d5 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/defer_gets_stripped_out.graphql b/apollo-federation/tests/query_plan/supergraphs/defer_gets_stripped_out.graphql index cda9ad539f..ec6c89f59b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/defer_gets_stripped_out.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/defer_gets_stripped_out.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 468d7f03b0a1c21793bcfc5e41d6ddeea4e94ed1 +# Composed from subgraphs with hash: 3e20191edc9bb3ef7d687ce811c347718e6e4100 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/does_not_error_on_some_complex_fetch_group_dependencies.graphql b/apollo-federation/tests/query_plan/supergraphs/does_not_error_on_some_complex_fetch_group_dependencies.graphql index dbc5271859..950263d878 100644 --- a/apollo-federation/tests/query_plan/supergraphs/does_not_error_on_some_complex_fetch_group_dependencies.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/does_not_error_on_some_complex_fetch_group_dependencies.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: f4f751d2b348c0947b2f1dbca4cea1c987ff7d02 +# Composed from subgraphs with hash: 604dead6fd018b16380a1abf51ba199acbdb82f2 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/does_not_error_out_handling_fragments_when_interface_subtyping_is_involved.graphql b/apollo-federation/tests/query_plan/supergraphs/does_not_error_out_handling_fragments_when_interface_subtyping_is_involved.graphql index 585bd1fea2..5137fdf735 100644 --- a/apollo-federation/tests/query_plan/supergraphs/does_not_error_out_handling_fragments_when_interface_subtyping_is_involved.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/does_not_error_out_handling_fragments_when_interface_subtyping_is_involved.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: b62548892fe6cba44778c9e402abb8ddf8edbac9 +# Composed from subgraphs with hash: 85a2b796ad6f1553ddbc0bf5f4722e602f475090 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -47,6 +49,8 @@ interface IB v1: Int! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/does_not_evaluate_plans_relying_on_a_key_field_to_fetch_that_same_field.graphql b/apollo-federation/tests/query_plan/supergraphs/does_not_evaluate_plans_relying_on_a_key_field_to_fetch_that_same_field.graphql index a55159cae8..674a08c9f2 100644 --- a/apollo-federation/tests/query_plan/supergraphs/does_not_evaluate_plans_relying_on_a_key_field_to_fetch_that_same_field.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/does_not_evaluate_plans_relying_on_a_key_field_to_fetch_that_same_field.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 15c059c34b90d54a9c27d2ad67c89307a1280a1f +# Composed from subgraphs with hash: e4cc087ad60a261439df7beadccbe9b4a31eaa8d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_for_typename.graphql b/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_for_typename.graphql index 14f041319c..97189f9365 100644 --- a/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_for_typename.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_for_typename.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 43bf934e8826f0b4d17867095ea2025fb352287a +# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I y: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is_requested.graphql b/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is_requested.graphql index 14f041319c..97189f9365 100644 --- a/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is_requested.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/does_not_rely_on_an_interface_object_directly_if_a_specific_implementation_is_requested.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 43bf934e8826f0b4d17867095ea2025fb352287a +# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I y: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql b/apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql index ab27753e5d..f62dcb21b5 100644 --- a/apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/ensures_sanitization_applies_repeatedly.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 39d8b2af4d4c017bda2b76a66d2128757ae8ac5d +# Composed from subgraphs with hash: 5a37dc86c56cab4caab8bb5d9606eb4c7df0d804 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql b/apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql index c107e04990..923bde8335 100644 --- a/apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/field_covariance_and_type_explosion.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: cea83739e9f98b70a1185463f8b9c3c17f2064c8 +# Composed from subgraphs with hash: 2254345d779de0a2850b6c7a5db722f1989a774c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -26,6 +28,8 @@ interface Interface field: Interface } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/fragment_with_intersecting_parent_type_and_directive_condition.graphql b/apollo-federation/tests/query_plan/supergraphs/fragment_with_intersecting_parent_type_and_directive_condition.graphql index 4417e0ea0b..1e2a373495 100644 --- a/apollo-federation/tests/query_plan/supergraphs/fragment_with_intersecting_parent_type_and_directive_condition.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/fragment_with_intersecting_parent_type_and_directive_condition.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 059bed56ce74dbb40643a1561b79514872f0ab60 +# Composed from subgraphs with hash: e2393609250b71261acc4089006bc8a14627d488 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -35,6 +37,8 @@ interface I2 title: String } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/fragments_that_share_a_hash_but_are_not_identical_generate_their_own_fragment_definitions.graphql b/apollo-federation/tests/query_plan/supergraphs/fragments_that_share_a_hash_but_are_not_identical_generate_their_own_fragment_definitions.graphql index eaf9c3e94d..a745ed6263 100644 --- a/apollo-federation/tests/query_plan/supergraphs/fragments_that_share_a_hash_but_are_not_identical_generate_their_own_fragment_definitions.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/fragments_that_share_a_hash_but_are_not_identical_generate_their_own_fragment_definitions.graphql @@ -1,16 +1,18 @@ -# Composed from subgraphs with hash: 9d07e44c7cffd48b0677fce186f5ba41a864bc13 +# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } directive @custom on FRAGMENT_SPREAD | INLINE_FRAGMENT +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,6 +39,8 @@ type B z: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql b/apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql index f43bbbed3d..9b7600e8a4 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handle_subgraph_with_hypen_in_the_name.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 6655be753e0c804f83461f143965a01f3a040e1d +# Composed from subgraphs with hash: c2ea0958d4d930ae3bf3535692a966af9f3af063 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql b/apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql index 1e9922d407..3a1ae9fab2 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handle_very_non_graph_subgraph_name.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 3d0f2fed8651dd6a1c0d4b7bb3c94989781c8b68 +# Composed from subgraphs with hash: 2ad000b79369f5b833cec8d6e5e62e99db89585c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_case_of_key_chains_in_parallel_requires.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_case_of_key_chains_in_parallel_requires.graphql index fc9d503e9c..fd7953bfd3 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_case_of_key_chains_in_parallel_requires.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_case_of_key_chains_in_parallel_requires.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 31c0bf4aeea8c73b60a47e91ea9bcc475127782a +# Composed from subgraphs with hash: 6f7dd1a877d5de533c98948e91197cb3526ed446 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_fragments_with_interface_field_subtyping.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_fragments_with_interface_field_subtyping.graphql index 5bdeacfe85..98032aadc8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_fragments_with_interface_field_subtyping.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_fragments_with_interface_field_subtyping.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: ed9e25c79dbfc98582011fc534e6998d77ec556c +# Composed from subgraphs with hash: cd5671782860ad7353fd02dfb07664cdc89a5809 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ interface I other: I! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_mix_of_fragments_indirection_and_unions.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_mix_of_fragments_indirection_and_unions.graphql index 9ceaef8378..2ff8417e3b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_mix_of_fragments_indirection_and_unions.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_mix_of_fragments_indirection_and_unions.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 857413268aa91cd215bfa7f9a717a01afcb77048 +# Composed from subgraphs with hash: 0b61d90468ba75e031fdc3c1abe0ac8e87674cc7 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -39,6 +41,8 @@ type Child id: ID! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_multiple_conditions_on_abstract_types.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_conditions_on_abstract_types.graphql new file mode 100644 index 0000000000..76280a249e --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_conditions_on_abstract_types.graphql @@ -0,0 +1,110 @@ +# Composed from subgraphs with hash: 2bb5218456c710b3aeaf9c5a9a7d87eee258917f +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +type Book implements Product + @join__implements(graph: PRODUCTS, interface: "Product") + @join__implements(graph: REVIEWS, interface: "Product") + @join__type(graph: BOOKS, key: "id") + @join__type(graph: PRODUCTS, key: "id") + @join__type(graph: REVIEWS, key: "id") +{ + id: ID! + title: String @join__field(graph: BOOKS) + sku: String @join__field(graph: PRODUCTS) + dimensions: ProductDimension @join__field(graph: PRODUCTS) + reviews: [Review!]! @join__field(graph: REVIEWS) +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + BOOKS @join__graph(name: "books", url: "none") + MAGAZINES @join__graph(name: "magazines", url: "none") + PRODUCTS @join__graph(name: "products", url: "none") + REVIEWS @join__graph(name: "reviews", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Magazine implements Product + @join__implements(graph: PRODUCTS, interface: "Product") + @join__implements(graph: REVIEWS, interface: "Product") + @join__type(graph: MAGAZINES, key: "id") + @join__type(graph: PRODUCTS, key: "id") + @join__type(graph: REVIEWS, key: "id") +{ + id: ID! + title: String @join__field(graph: MAGAZINES) + sku: String @join__field(graph: PRODUCTS) + dimensions: ProductDimension @join__field(graph: PRODUCTS) + reviews: [Review!]! @join__field(graph: REVIEWS) +} + +interface Product + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) +{ + id: ID! + sku: String @join__field(graph: PRODUCTS) + dimensions: ProductDimension @join__field(graph: PRODUCTS) + reviews: [Review!]! @join__field(graph: REVIEWS) +} + +type ProductDimension + @join__type(graph: PRODUCTS) +{ + size: String + weight: Float +} + +type Query + @join__type(graph: BOOKS) + @join__type(graph: MAGAZINES) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) +{ + products: [Product] @join__field(graph: PRODUCTS) +} + +type Review + @join__type(graph: REVIEWS) +{ + id: Int! + body: String! + product: Product +} diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql index 78d523b512..9a5c273ce0 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_multiple_requires_involving_different_nestedness.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: de3bbad205ccca07c2a0fe699d358368241993b7 +# Composed from subgraphs with hash: 6120a9371b90c378d13d977d19bad9e17c6875cb schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -31,6 +33,8 @@ type Item computed2: String @join__field(graph: SUBGRAPH2, requires: "user { value }") } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_non_intersecting_fragment_conditions.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_non_intersecting_fragment_conditions.graphql index 134bb9a299..4be5ebe264 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_non_intersecting_fragment_conditions.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_non_intersecting_fragment_conditions.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 621fe405b7bc52f4f5f0102b2aaf8a374504eee3 +# Composed from subgraphs with hash: cc0d8d93561b6e20d87ae1541b16037be17333ef schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -42,6 +44,8 @@ interface Fruit edible: Boolean! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_non_matching_value_types_under_interface_field.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_non_matching_value_types_under_interface_field.graphql index 27487542e0..10cc68d004 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_non_matching_value_types_under_interface_field.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_non_matching_value_types_under_interface_field.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 688f2dad6c47c75df28f6cdd47bb6cc242311192 +# Composed from subgraphs with hash: 9959b044b8b84e47c4559354eb1d87299be28495 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -26,6 +28,8 @@ interface I s: S } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_query_of_an_interface_field_for_a_specific_implementation_when_query_starts_with_interface_object.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_query_of_an_interface_field_for_a_specific_implementation_when_query_starts_with_interface_object.graphql index 14f041319c..97189f9365 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_query_of_an_interface_field_for_a_specific_implementation_when_query_starts_with_interface_object.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_query_of_an_interface_field_for_a_specific_implementation_when_query_starts_with_interface_object.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 43bf934e8826f0b4d17867095ea2025fb352287a +# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I y: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_requires_from_supergraph.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_requires_from_supergraph.graphql index a3f47c437f..4afa0074ca 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_requires_from_supergraph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_requires_from_supergraph.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 46a2d6c6cf9956c08daa5b3faa018245cb5f9cfe +# Composed from subgraphs with hash: c980fbf8b4a77ab38f828719197c4878658aa92c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ interface I name: String } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_root_operation_shareable_in_many_subgraphs.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_root_operation_shareable_in_many_subgraphs.graphql index 76ccd6875f..9c2c5435dd 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_root_operation_shareable_in_many_subgraphs.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_root_operation_shareable_in_many_subgraphs.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 23796fabaf5b788d4a3df5cd4043263f2d825828 +# Composed from subgraphs with hash: dece71b67cd54bf5e3bf43988e99a638876d52e5 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_simple_requires.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_simple_requires.graphql index 2620fedb2f..a7bbfe095a 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_simple_requires.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_simple_requires.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: f4e377fc3c287e29c8acbbdb5614ae18542b310d +# Composed from subgraphs with hash: a2964ea427e664c7c007a258b54d68db9abae8f4 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_spread_unions_correctly.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_spread_unions_correctly.graphql index f5ffa85f3b..d0d026a6f3 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_spread_unions_correctly.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_spread_unions_correctly.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: cac8f01c24f23138d810fe086c24cf3e3c9b724e +# Composed from subgraphs with hash: 361121049969085d46eae818d8c9d8da18d3cf6c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -45,6 +47,8 @@ type C c2: Int @join__field(graph: SUBGRAPH2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/handles_types_with_no_common_supertype_at_the_same_merge_at.graphql b/apollo-federation/tests/query_plan/supergraphs/handles_types_with_no_common_supertype_at_the_same_merge_at.graphql index 7fd7b3ba28..15ae958653 100644 --- a/apollo-federation/tests/query_plan/supergraphs/handles_types_with_no_common_supertype_at_the_same_merge_at.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/handles_types_with_no_common_supertype_at_the_same_merge_at.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 914913e8f6d05467f239b8984f8875df35e9ec9c +# Composed from subgraphs with hash: 98f941b67e6dc18205c3148808a6bce2b64c775f schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -38,6 +40,8 @@ type Foo y: Int @join__field(graph: SUBGRAPH2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction.graphql b/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction.graphql index a1130db8d5..4c89d271df 100644 --- a/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: d0de8bd2759f8c48a97e2e09f5b1cd8bad7e6e78 +# Composed from subgraphs with hash: 6ac1ca5a8c7d1596024e97bb378f1f829788fd71 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -58,6 +60,8 @@ interface I2 v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction_but_no_need_to_type_explode.graphql b/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction_but_no_need_to_type_explode.graphql index 86b278865d..5433f33dcf 100644 --- a/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction_but_no_need_to_type_explode.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/interface_interface_interaction_but_no_need_to_type_explode.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 7089406367e2cc6e6d0864f78d01407a184ffa92 +# Composed from subgraphs with hash: 5073ac23b2d2f5657028261e837e26e4370a3cf4 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -58,6 +60,8 @@ interface I2 v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction.graphql b/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction.graphql index fdd3f10953..11afc89b98 100644 --- a/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 34d04b226bb8991dcd8a063656dbf04b18555a0c +# Composed from subgraphs with hash: 43ca669a61b756dfac8bec5822d87393bc9f3544 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -48,6 +50,8 @@ interface I v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction_but_no_need_to_type_explode.graphql b/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction_but_no_need_to_type_explode.graphql index 996c6012dc..60c26dd523 100644 --- a/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction_but_no_need_to_type_explode.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/interface_union_interaction_but_no_need_to_type_explode.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 5c7d149e1ef1b997ab3395128beb7403f1f11dfe +# Composed from subgraphs with hash: 2548adb14fdc2eacf229834ee87327873c15cb8d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql b/apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql index 9cb1ecc9b4..fcbc7f2aa3 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_allow_providing_fields_for_only_some_subtype.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 84c56a8f07d5763e8dfa886575ed2a5df80bbaf6 +# Composed from subgraphs with hash: 7225f76e0549cfa72d92ea3d954fbfa4f5325cff schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ interface I b: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_avoid_fragments_usable_only_once.graphql b/apollo-federation/tests/query_plan/supergraphs/it_avoid_fragments_usable_only_once.graphql index 809c44d072..629427d8e7 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_avoid_fragments_usable_only_once.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_avoid_fragments_usable_only_once.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 61f7aebc1284fd8dc840de895884ee5b7fd836c2 +# Composed from subgraphs with hash: 57cf5fd9ee4bd6e69d9976aef7ff289a74c757c4 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_with_lists.graphql b/apollo-federation/tests/query_plan/supergraphs/it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_with_lists.graphql index b18286ce84..b989cee25c 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_with_lists.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_avoids_buffering_interface_object_results_that_may_have_to_be_filtered_with_lists.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 6509cee656ab4776fa9304fdc43ae726848fb0e0 +# Composed from subgraphs with hash: 885b71f276ed574d24895695bddb2bd130640f65 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -46,6 +48,8 @@ interface I expansiveField: String @join__field(graph: S1) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql index a4bcc63125..45bca01a72 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_can_require_at_inaccessible_fields.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 78f8e2391131a01115d7025d6f7aae3763f91d5e +# Composed from subgraphs with hash: 49f8443a5ff323a1ffed6b7122a1d61e6225d235 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) { query: Query @@ -9,9 +9,11 @@ schema directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -23,6 +25,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_needlessly_consider_options_for_typename.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_needlessly_consider_options_for_typename.graphql index d4597c4e76..82876af15e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_does_not_needlessly_consider_options_for_typename.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_needlessly_consider_options_for_typename.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 147f9156540134dbaf7c6047530df577b0c28ced +# Composed from subgraphs with hash: ee7dda36e9dc663f9750f86da48822e8f6f6ea8f schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-router/src/testdata/minimal_fed2_supergraph.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql similarity index 61% rename from apollo-router/src/testdata/minimal_fed2_supergraph.graphql rename to apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql index 896c6f95e4..206487ca49 100644 --- a/apollo-router/src/testdata/minimal_fed2_supergraph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql @@ -1,13 +1,16 @@ +# Composed from subgraphs with hash: b409284c35003f62c7c83675734478b1970effb2 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -19,10 +22,13 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { - SUBGRAPH_A @join__graph(name: "subgraph-a", url: "http://graphql.subgraph-a.svc.cluster.local:4000") + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") } scalar link__Import @@ -39,6 +45,9 @@ enum link__Purpose { EXECUTION } -type Query @join__type(graph: SUBGRAPH_A) { - me: String @join__field(graph: SUBGRAPH_A) +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + hello: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") } diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgraph.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgraph.graphql index 7230a3c168..d0358af17b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgraph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_try_to_apply_fragments_that_are_not_valid_for_the_subgraph.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 7216df3d57d6ea85890cb4557bc1400d2a32faf0 +# Composed from subgraphs with hash: 11ffa462805ad28daecca693ed1a45d931d3cac8 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -28,6 +30,8 @@ interface I b: Int @join__field(graph: SUBGRAPH2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_executes_mutation_operations_in_sequence.graphql b/apollo-federation/tests/query_plan/supergraphs/it_executes_mutation_operations_in_sequence.graphql index 2dbf99dd61..c8a3b06e68 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_executes_mutation_operations_in_sequence.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_executes_mutation_operations_in_sequence.graphql @@ -1,15 +1,17 @@ -# Composed from subgraphs with hash: fea1be241ad3c07fc4036893012a86c4e17ea159 +# Composed from subgraphs with hash: ff6534336a58b9a72232e43fdb66c4769ac4ae66 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query mutation: Mutation } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -21,6 +23,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql index 1ef09945bd..b410e60027 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handes_diamond_shape_depedencies.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: c6a3588d5bfe556f3de3cf2f3fa2165704f9206d +# Composed from subgraphs with hash: 2a90ec602bc41c24462757dc66b2e37e1d96dbf4 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql index 49e3180b5b..c7102b37a9 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_a_simple_at_requires_triggered_within_a_conditional.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 0e4eac66a889724333f86a54a0647cc4f694f511 +# Composed from subgraphs with hash: a95f4fdeb4fa54d3e87c7a5eb71952eba3efdf28 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql index 49e3180b5b..c7102b37a9 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_triggered_conditionally.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 0e4eac66a889724333f86a54a0647cc4f694f511 +# Composed from subgraphs with hash: a95f4fdeb4fa54d3e87c7a5eb71952eba3efdf28 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql index 020d3b8da0..1b13a0feb0 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_an_at_requires_where_multiple_conditional_are_involved.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 2ea9dba0e4a9f205821228485cdd112ca0b0c17a +# Composed from subgraphs with hash: b43c835356ccf4afa94c6ad48206c70f22f39ffc schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,6 +39,8 @@ type B c: Int @join__field(graph: SUBGRAPH3, requires: "required") } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql index 8a382fa437..8fbe10543a 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_complex_require_chain.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 722cd56dba6c8500f961ba2c48cc9f643b2fda98 +# Composed from subgraphs with hash: eeb0b0963b210e2f82e6f40f535037882afb96db schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -38,6 +40,8 @@ type Inner4Type inner4_nested: Int! @join__field(graph: SUBGRAPH5, requires: "inner4_required") } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs.graphql index 836e43cc0a..39f730f702 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_subtyping_relation_differs.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 85ca2a0ec950344b1f685478ac0ea4f36e5b7692 +# Composed from subgraphs with hash: 0545139260d6549f4e32906bce43c37742fdff80 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -36,6 +38,8 @@ type Inner implements I w: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs.graphql index af1d65d24e..8706c2af1e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragment_rebasing_in_a_subgraph_where_some_union_membership_relation_differs.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 532639290329813c32101df1b46a23c760db68fc +# Composed from subgraphs with hash: 2be5cb3476eaeac718ff9d77f717b70fe9f344a2 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type Inner w: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragments_with_one_non_leaf_field.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragments_with_one_non_leaf_field.graphql index eaf9c3e94d..a745ed6263 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_fragments_with_one_non_leaf_field.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_fragments_with_one_non_leaf_field.graphql @@ -1,16 +1,18 @@ -# Composed from subgraphs with hash: 9d07e44c7cffd48b0677fce186f5ba41a864bc13 +# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } directive @custom on FRAGMENT_SPREAD | INLINE_FRAGMENT +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,6 +39,8 @@ type B z: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_in_nested_entity.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_in_nested_entity.graphql index 9958fad35f..fe5bef73a1 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_in_nested_entity.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_in_nested_entity.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: e8059ef7894398e0a461b58b839372a8b3a61d64 +# Composed from subgraphs with hash: 3881ebaea69bd73941780a88cffefd7f909c77d1 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -47,6 +49,8 @@ interface I a: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_input_rewrites_when_cloning_dependency_graph.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_input_rewrites_when_cloning_dependency_graph.graphql index 398d4b25e2..41b8deead2 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_input_rewrites_when_cloning_dependency_graph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_interface_object_input_rewrites_when_cloning_dependency_graph.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: ebabfd0be0356580cd148a45b86e2db82b29d3c3 +# Composed from subgraphs with hash: 3799eb12e6387413ef90a73d8848b5c48e40cbca schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,6 +31,8 @@ interface I i3: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql index 207d9de6f3..0d81bb4ee6 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_longer_require_chain.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 125a036421e2f6b9aa7e2c1bcabc29a4e3aa32cf +# Composed from subgraphs with hash: 00470b1d89f783a11f9b05f82d8377c0d5e3f89d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_with_multiple_fetches.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_with_multiple_fetches.graphql index 8e4d26a88e..57e31110e1 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_with_multiple_fetches.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_with_multiple_fetches.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 461e4a611a1faf2558d6ee6e3de4af24a043fc16 +# Composed from subgraphs with hash: 55b2cd6d674b743ae99fa918dce065759899f51d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -29,6 +31,8 @@ interface I name: String! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql index e598d84a1b..8beeb189bd 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_multiple_requires_within_the_same_entity_fetch.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: dd69cd7ccb9070ab8a3de08ead3e41eacce180d1 +# Composed from subgraphs with hash: 151440cab35934f002ebab673ce5f8bd86966772 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -28,6 +30,8 @@ interface I g: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation.graphql index eaf9c3e94d..a745ed6263 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_nested_fragment_generation.graphql @@ -1,16 +1,18 @@ -# Composed from subgraphs with hash: 9d07e44c7cffd48b0677fce186f5ba41a864bc13 +# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } directive @custom on FRAGMENT_SPREAD | INLINE_FRAGMENT +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,6 +39,8 @@ type B z: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql new file mode 100644 index 0000000000..206487ca49 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql @@ -0,0 +1,53 @@ +# Composed from subgraphs with hash: b409284c35003f62c7c83675734478b1970effb2 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + hello: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql index 251390eef3..3d4964b712 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_require_chain_not_ending_in_original_group.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: fadb16531ae6d8fe34df35cd8993071bd83b8160 +# Composed from subgraphs with hash: ff92849ceafa9aaccab960b8b5ce0e98a13e6a00 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_requires_on_concrete_type_of_field_provided_by_interface_object.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_requires_on_concrete_type_of_field_provided_by_interface_object.graphql index b0306ba46d..aae7fd038a 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_requires_on_concrete_type_of_field_provided_by_interface_object.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_requires_on_concrete_type_of_field_provided_by_interface_object.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: c6d2827cd86e25fe8093045c7a65ad35e1ca5101 +# Composed from subgraphs with hash: 209a0436ca69cb640450ef4fc7c204a5b5fc6058 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -45,6 +47,8 @@ interface I x: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql index 15556cf8e9..b076bc92fe 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_simple_require_chain.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 49a33e65faa01be4bcd2f444dd5f10f439a286a8 +# Composed from subgraphs with hash: 244af51d2d8ba7d87e4b9fec74bbb49dc7b00e4d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_identifies_and_reuses_equivalent_fragments_that_arent_identical.graphql b/apollo-federation/tests/query_plan/supergraphs/it_identifies_and_reuses_equivalent_fragments_that_arent_identical.graphql index eaf9c3e94d..a745ed6263 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_identifies_and_reuses_equivalent_fragments_that_arent_identical.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_identifies_and_reuses_equivalent_fragments_that_arent_identical.graphql @@ -1,16 +1,18 @@ -# Composed from subgraphs with hash: 9d07e44c7cffd48b0677fce186f5ba41a864bc13 +# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } directive @custom on FRAGMENT_SPREAD | INLINE_FRAGMENT +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,6 +39,8 @@ type B z: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_migrates_skip_include.graphql b/apollo-federation/tests/query_plan/supergraphs/it_migrates_skip_include.graphql index eaf9c3e94d..a745ed6263 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_migrates_skip_include.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_migrates_skip_include.graphql @@ -1,16 +1,18 @@ -# Composed from subgraphs with hash: 9d07e44c7cffd48b0677fce186f5ba41a864bc13 +# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } directive @custom on FRAGMENT_SPREAD | INLINE_FRAGMENT +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,6 +39,8 @@ type B z: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preservers_aliased_typename.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preservers_aliased_typename.graphql index 4a98373896..adc483ebe7 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_preservers_aliased_typename.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_preservers_aliased_typename.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: b5d9704c92902a18f288f76f368385798d68f1f1 +# Composed from subgraphs with hash: d7fe5a716fee436faefb289f7ba4a8bd05bd7d34 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_is_reused.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_is_reused.graphql index d6ca67b84f..9aa130fc7f 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_is_reused.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_is_reused.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: b6ebc4ffa76637f33269672e1a49739b3b7091a8 +# Composed from subgraphs with hash: 75955b009750aae84f92b194eddcb553f0e44656 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_not_used.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_not_used.graphql index d6ca67b84f..9aa130fc7f 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_not_used.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_preserves_directives_when_fragment_not_used.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: b6ebc4ffa76637f33269672e1a49739b3b7091a8 +# Composed from subgraphs with hash: 75955b009750aae84f92b194eddcb553f0e44656 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_preserves_nested_fragments_when_outer_one_has_directives_and_is_eliminated.graphql b/apollo-federation/tests/query_plan/supergraphs/it_preserves_nested_fragments_when_outer_one_has_directives_and_is_eliminated.graphql index fd0936744c..a12329a27d 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_preserves_nested_fragments_when_outer_one_has_directives_and_is_eliminated.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_preserves_nested_fragments_when_outer_one_has_directives_and_is_eliminated.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 2bb9de0177faf2f6dbf5fe84f5dd6e07fbc61635 +# Composed from subgraphs with hash: f580fdce2d5df285d6e12633d4355b51e2fd52fd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql b/apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql index 44f27a7576..66be42e56a 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_require_of_multiple_field_when_one_is_also_a_key_to_reach_another.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 57efb2956792ca0eb40a9df16cbd08aad77cfd44 +# Composed from subgraphs with hash: 3766e23446d1f1881fb155fefa8036726c6f34c4 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_respects_generate_query_fragments_option.graphql b/apollo-federation/tests/query_plan/supergraphs/it_respects_generate_query_fragments_option.graphql index eaf9c3e94d..a745ed6263 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_respects_generate_query_fragments_option.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_respects_generate_query_fragments_option.graphql @@ -1,16 +1,18 @@ -# Composed from subgraphs with hash: 9d07e44c7cffd48b0677fce186f5ba41a864bc13 +# Composed from subgraphs with hash: 62d7e50e5ef7c204246ba7c0cefcef3e9e5bef0c schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } directive @custom on FRAGMENT_SPREAD | INLINE_FRAGMENT +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -37,6 +39,8 @@ type B z: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql index fabd270b4b..a7d3b59025 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_on_interfaces.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 8f950745a224d72bf86dc6d2c6a66c76ed7c5fe0 +# Composed from subgraphs with hash: d654244e34da1e73ea47f5325e371614408d38ae schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -26,6 +28,8 @@ interface I v: Value } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql index c92e397384..bb44229f8d 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_on_unions.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 627a6f4e651e7ff691b6c7d4104a68dd562dd7b7 +# Composed from subgraphs with hash: 87210bff4bf720ff0fe68c22dae1d3e5520d7980 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_1.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_1.graphql index 47f05c483a..3b5fe2a129 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_1.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_1.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: e0dfa4222558f28e0ecb3e26077458471e69267b +# Composed from subgraphs with hash: 18fc379a3170731963d1ec9f54f8b002b0f5d874 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -62,6 +64,8 @@ interface Foo child2: Foo } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved.graphql index 64e2fd6e3b..ef1c275632 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_fragments_when_only_the_nested_fragment_gets_preserved.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 425c9d41052b9208347cddf5826dfcaed779900a +# Composed from subgraphs with hash: 197c4ea5c04d17ec60cb084c49efe2f9d662079b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql index bc8611b65a..581e7c764b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_nested_provides.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 952c14738ea71875811bacb2c3fdae24798ea16b +# Composed from subgraphs with hash: fb1513832764f051dd663a966309809fb58e4519 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql b/apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql index eb282becb9..1b20db25a4 100644 --- a/apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/it_works_with_type_condition_even_for_types_only_reachable_by_the_at_provides.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 3c2e16964e0f336b33ea0c717a1a45a4f11e6596 +# Composed from subgraphs with hash: 0556f39f18edcd446ffee2af4cec39d408bf7b7d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -35,6 +37,8 @@ interface I a: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/key_where_at_external_is_not_at_top_level_of_selection_of_requires.graphql b/apollo-federation/tests/query_plan/supergraphs/key_where_at_external_is_not_at_top_level_of_selection_of_requires.graphql index b35c49754a..c5c7c136a3 100644 --- a/apollo-federation/tests/query_plan/supergraphs/key_where_at_external_is_not_at_top_level_of_selection_of_requires.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/key_where_at_external_is_not_at_top_level_of_selection_of_requires.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: c1a3645bcd1d5de8ffaaafc2db246bb829597152 +# Composed from subgraphs with hash: 3035e486a3ec94c91ff841c2eb464b287fead177 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_order.graphql b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_order.graphql index 9447973fc1..9d3b36f76a 100644 --- a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_order.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_order.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 58185941858c72bbf45ca3d14674013b6a6c0ce7 +# Composed from subgraphs with hash: 1cce0a8dc674e651027f8d368504edb388f9b239 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type Hello goodbye: String! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_quantity.graphql b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_quantity.graphql index bf44f896c5..80f6562f18 100644 --- a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_quantity.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_differing_quantity.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: f4972a81ae6443a316826d336e1947a5aad4d2e6 +# Composed from subgraphs with hash: fbbe470b5e40af130de42043f74772ec30ee60ff schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type Hello goodbye: String! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_identical.graphql b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_identical.graphql index bf44f896c5..80f6562f18 100644 --- a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_identical.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_multiple_applications_identical.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: f4972a81ae6443a316826d336e1947a5aad4d2e6 +# Composed from subgraphs with hash: fbbe470b5e40af130de42043f74772ec30ee60ff schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type Hello goodbye: String! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_with_fragment.graphql b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_with_fragment.graphql index bf44f896c5..80f6562f18 100644 --- a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_with_fragment.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_with_fragment.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: f4972a81ae6443a316826d336e1947a5aad4d2e6 +# Composed from subgraphs with hash: fbbe470b5e40af130de42043f74772ec30ee60ff schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type Hello goodbye: String! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_without_fragment.graphql b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_without_fragment.graphql index bf44f896c5..80f6562f18 100644 --- a/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_without_fragment.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/merging_skip_and_include_directives_without_fragment.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: f4972a81ae6443a316826d336e1947a5aad4d2e6 +# Composed from subgraphs with hash: fbbe470b5e40af130de42043f74772ec30ee60ff schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type Hello goodbye: String! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/multiplication_overflow_in_reduce_options_if_needed.graphql b/apollo-federation/tests/query_plan/supergraphs/multiplication_overflow_in_reduce_options_if_needed.graphql index 0b6baf2e68..42f3b4b125 100644 --- a/apollo-federation/tests/query_plan/supergraphs/multiplication_overflow_in_reduce_options_if_needed.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/multiplication_overflow_in_reduce_options_if_needed.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: bf831e2e6890f60e5c8e93bc52ce549323cb23e8 +# Composed from subgraphs with hash: 91d4d0661d413b60ae2ed463c074c4180d7b849e schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/nested_fragment_with_interseting_parent_type_and_directive_condition.graphql b/apollo-federation/tests/query_plan/supergraphs/nested_fragment_with_interseting_parent_type_and_directive_condition.graphql index 4417e0ea0b..1e2a373495 100644 --- a/apollo-federation/tests/query_plan/supergraphs/nested_fragment_with_interseting_parent_type_and_directive_condition.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/nested_fragment_with_interseting_parent_type_and_directive_condition.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 059bed56ce74dbb40643a1561b79514872f0ab60 +# Composed from subgraphs with hash: e2393609250b71261acc4089006bc8a14627d488 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -35,6 +37,8 @@ interface I2 title: String } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/non_adjacent_mutations_do_not_get_merged.graphql b/apollo-federation/tests/query_plan/supergraphs/non_adjacent_mutations_do_not_get_merged.graphql index e99930397b..ae84cc8ed8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/non_adjacent_mutations_do_not_get_merged.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/non_adjacent_mutations_do_not_get_merged.graphql @@ -1,15 +1,17 @@ -# Composed from subgraphs with hash: 2655e7da6754e73955fece01d7cbb5f21085bdbb +# Composed from subgraphs with hash: 54adb76945715a2ce0e068d2635d71ca4166b289 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query mutation: Mutation } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -30,6 +32,8 @@ type Foo baz: Int @join__field(graph: SUBGRAPHB) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/only_uses_an_interface_object_if_it_can.graphql b/apollo-federation/tests/query_plan/supergraphs/only_uses_an_interface_object_if_it_can.graphql index 14f041319c..97189f9365 100644 --- a/apollo-federation/tests/query_plan/supergraphs/only_uses_an_interface_object_if_it_can.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/only_uses_an_interface_object_if_it_can.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 43bf934e8826f0b4d17867095ea2025fb352287a +# Composed from subgraphs with hash: da551e74f6885542cefc64ed31d2b00579dce2cd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I y: Int @join__field(graph: S2) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql b/apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql index 74a64a0313..dc51cbcdcc 100644 --- a/apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/pick_keys_that_minimize_fetches.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 87d7f4f15c5d3342bb4073f6926e35ba9d0cf435 +# Composed from subgraphs with hash: 454b3fc41adce04fc670f06030743d521d09096d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -35,6 +37,8 @@ type Currency sign: String! } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/redundant_typename_for_inline_fragments_without_type_condition.graphql b/apollo-federation/tests/query_plan/supergraphs/redundant_typename_for_inline_fragments_without_type_condition.graphql index 0b9050d640..923d850187 100644 --- a/apollo-federation/tests/query_plan/supergraphs/redundant_typename_for_inline_fragments_without_type_condition.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/redundant_typename_for_inline_fragments_without_type_condition.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: d267b3f18f7736e1a3e39a951696c318d1f46a34 +# Composed from subgraphs with hash: f02b27ab5f92e1e70c07bf7d5ce65e620637ef35 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_false.graphql b/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_false.graphql index 42f3923d30..003c7fc0ec 100644 --- a/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_false.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_false.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: efdbea0791d6ac8577eb1b7b63618adec547f1c8 +# Composed from subgraphs with hash: 4fc759c1b39c54d520a6868db43813e4a38bbaf3 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type A y: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_true.graphql b/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_true.graphql index 42f3923d30..003c7fc0ec 100644 --- a/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_true.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/respects_query_planner_option_reuse_query_fragments_true.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: efdbea0791d6ac8577eb1b7b63618adec547f1c8 +# Composed from subgraphs with hash: 4fc759c1b39c54d520a6868db43813e4a38bbaf3 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -27,6 +29,8 @@ type A y: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql b/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql index a6d3125d50..6c750cf146 100644 --- a/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/selections_are_not_overwritten_after_removing_directives.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: d5699e8f9c56867b4a27e8b7b2e942a65af9c97b +# Composed from subgraphs with hash: a3e748206fa553b8f317912a3a411ee68b55db5d schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -36,6 +38,8 @@ type Foo bar: Bar } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/skip_type_explosion_early_if_unnecessary.graphql b/apollo-federation/tests/query_plan/supergraphs/skip_type_explosion_early_if_unnecessary.graphql index 2626014b39..c0db4184d4 100644 --- a/apollo-federation/tests/query_plan/supergraphs/skip_type_explosion_early_if_unnecessary.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/skip_type_explosion_early_if_unnecessary.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 92a7f0921bb3da20d67bc49b77d12eecef437e91 +# Composed from subgraphs with hash: 63eb1eb88aa472162170c24e74ca47c7c4ac1866 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -26,6 +28,8 @@ interface I s: S } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/subgraph_query_retains_the_query_variables_used_in_the_directives_applied_to_the_query.graphql b/apollo-federation/tests/query_plan/supergraphs/subgraph_query_retains_the_query_variables_used_in_the_directives_applied_to_the_query.graphql index 13112ebd1d..67fa096ee1 100644 --- a/apollo-federation/tests/query_plan/supergraphs/subgraph_query_retains_the_query_variables_used_in_the_directives_applied_to_the_query.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/subgraph_query_retains_the_query_variables_used_in_the_directives_applied_to_the_query.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 97c42b7140502842f2f75aed633e82397ef462c4 +# Composed from subgraphs with hash: 64759e825a65c99c54fedcbf511cc7bf0735d4e2 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -22,6 +24,8 @@ directive @link(url: String, as: String, for: link__Purpose, import: [link__Impo directive @withArgs(arg1: String) on QUERY +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_at_the_operation_level_are_passed_down_to_subgraph_queries.graphql b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_at_the_operation_level_are_passed_down_to_subgraph_queries.graphql index 25a1e12239..f2012f3f04 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_at_the_operation_level_are_passed_down_to_subgraph_queries.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_at_the_operation_level_are_passed_down_to_subgraph_queries.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 0574a22aea40ebfdb91c4ea931889f285c43a04c +# Composed from subgraphs with hash: a0524dbe1bbd3a7450a2e15f5c25c5cf2eed4242 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query mutation: Mutation @@ -9,9 +9,11 @@ schema directive @field on FIELD +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -35,6 +37,8 @@ type Foo baz: Int @join__field(graph: SUBGRAPHB) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_on_mutations_are_passed_down_to_subgraph_queries.graphql b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_on_mutations_are_passed_down_to_subgraph_queries.graphql index 25a1e12239..f2012f3f04 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_on_mutations_are_passed_down_to_subgraph_queries.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_on_mutations_are_passed_down_to_subgraph_queries.graphql @@ -1,7 +1,7 @@ -# Composed from subgraphs with hash: 0574a22aea40ebfdb91c4ea931889f285c43a04c +# Composed from subgraphs with hash: a0524dbe1bbd3a7450a2e15f5c25c5cf2eed4242 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query mutation: Mutation @@ -9,9 +9,11 @@ schema directive @field on FIELD +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -35,6 +37,8 @@ type Foo baz: Int @join__field(graph: SUBGRAPHB) } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_with_arguments_applied_on_queries_are_ok.graphql b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_with_arguments_applied_on_queries_are_ok.graphql index c59e350fed..5eb84bed66 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_if_directives_with_arguments_applied_on_queries_are_ok.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_if_directives_with_arguments_applied_on_queries_are_ok.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 20fe4c183c4983750398bd0be297f8ddc4251006 +# Composed from subgraphs with hash: 5338f8c604ab7c2103bdf58ee6752a40d4b62ed8 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -24,6 +26,8 @@ directive @noArgs on QUERY directive @withArgs(arg1: String) on QUERY +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_do_not_create_cycle_in_fetch_dependency_graph.graphql b/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_do_not_create_cycle_in_fetch_dependency_graph.graphql index aea8d395a8..82e69229f1 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_do_not_create_cycle_in_fetch_dependency_graph.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_do_not_create_cycle_in_fetch_dependency_graph.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 58cfa42df5c5f20fb0fbe43d4a506b3654439de1 +# Composed from subgraphs with hash: 0b567f1d7e0089a146e8499a2c5e412b78a98498 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_reset_cached_costs.graphql b/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_reset_cached_costs.graphql index c513bf6ad9..6b66fcbf6e 100644 --- a/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_reset_cached_costs.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/test_merging_fetches_reset_cached_costs.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 30529f3ff76bc917d12a637ede6e78ce39e7bca8 +# Composed from subgraphs with hash: 7c07647747a84fd6c839bb603948dddcadf654fd schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/trying_to_use_defer_with_a_subcription_results_in_an_error.graphql b/apollo-federation/tests/query_plan/supergraphs/trying_to_use_defer_with_a_subcription_results_in_an_error.graphql index 25f9d7d739..1d901c0ee1 100644 --- a/apollo-federation/tests/query_plan/supergraphs/trying_to_use_defer_with_a_subcription_results_in_an_error.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/trying_to_use_defer_with_a_subcription_results_in_an_error.graphql @@ -1,15 +1,17 @@ -# Composed from subgraphs with hash: 010102349764cd3dac7215c8cc5c825916b3b8db +# Composed from subgraphs with hash: 4c8b155cb8183b493b5c2862a2bd4ce0261642f9 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query subscription: Subscription } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -21,6 +23,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction.graphql b/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction.graphql index ccbe1edfa3..347f1bcfb8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 9355e80c20445d59cd2d9a8cd04db6a6bebb3ce0 +# Composed from subgraphs with hash: 336400b353f835910c178a10eb77371f8700396b schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -49,6 +51,8 @@ interface I v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction_but_no_need_to_type_explode.graphql b/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction_but_no_need_to_type_explode.graphql index 79d0e203e2..3dfc5d39b8 100644 --- a/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction_but_no_need_to_type_explode.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/union_interface_interaction_but_no_need_to_type_explode.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 16225ee3e1f3aaff3e1fe3e4cfe3298d8095f608 +# Composed from subgraphs with hash: 0457b8e67ae0ea99ead4c13318c4ac89e821aac3 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -48,6 +50,8 @@ interface I v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/union_union_interaction.graphql b/apollo-federation/tests/query_plan/supergraphs/union_union_interaction.graphql index 35a5d9f1c7..bb7bf23e98 100644 --- a/apollo-federation/tests/query_plan/supergraphs/union_union_interaction.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/union_union_interaction.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 0fdc118d2b5e2aed1f67357115b1614c2a230888 +# Composed from subgraphs with hash: 997336a5c7996069d8c10d0b9be46a72b46459c1 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -39,6 +41,8 @@ type C v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/union_union_interaction_but_no_need_to_type_explode.graphql b/apollo-federation/tests/query_plan/supergraphs/union_union_interaction_but_no_need_to_type_explode.graphql index 4a4292e66c..3dd79adf66 100644 --- a/apollo-federation/tests/query_plan/supergraphs/union_union_interaction_but_no_need_to_type_explode.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/union_union_interaction_but_no_need_to_type_explode.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 03fe71c2f384dc3668709b401037d4139e6cd4e5 +# Composed from subgraphs with hash: 241b1f7314833f7c2a97da8176e258c40322b0d7 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -39,6 +41,8 @@ type C v: Int } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql b/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql index bed3adbc67..a3668cb67b 100644 --- a/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/unnecessary_include_is_stripped_from_fragments.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: 07faf0f54a80eb7ef3e78f4fbe6382e135f6aac6 +# Composed from subgraphs with hash: 133c359820bd42cb8afd64ab1e02bb912b5d1746 schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -35,6 +37,8 @@ type Foo bar: Bar } +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/works_when_unset.graphql b/apollo-federation/tests/query_plan/supergraphs/works_when_unset.graphql index 0b6baf2e68..42f3b4b125 100644 --- a/apollo-federation/tests/query_plan/supergraphs/works_when_unset.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/works_when_unset.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: bf831e2e6890f60e5c8e93bc52ce549323cb23e8 +# Composed from subgraphs with hash: 91d4d0661d413b60ae2ed463c074c4180d7b849e schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-federation/tests/query_plan/supergraphs/works_with_key_chains.graphql b/apollo-federation/tests/query_plan/supergraphs/works_with_key_chains.graphql index 51ca7bb320..48816b3a72 100644 --- a/apollo-federation/tests/query_plan/supergraphs/works_with_key_chains.graphql +++ b/apollo-federation/tests/query_plan/supergraphs/works_with_key_chains.graphql @@ -1,14 +1,16 @@ -# Composed from subgraphs with hash: a5757c4964ac9dabd9a7dbd22a30a62f5d863033 +# Composed from subgraphs with hash: 4387e2f917c7748296b92799227648747e453bec schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { query: Query } +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE @@ -20,6 +22,8 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +scalar join__DirectiveArguments + scalar join__FieldSet enum join__Graph { diff --git a/apollo-router-benchmarks/Cargo.toml b/apollo-router-benchmarks/Cargo.toml index 6b8d060d88..38759d2feb 100644 --- a/apollo-router-benchmarks/Cargo.toml +++ b/apollo-router-benchmarks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-benchmarks" -version = "1.54.0" +version = "1.55.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-benchmarks/benches/fixtures/supergraph.graphql b/apollo-router-benchmarks/benches/fixtures/supergraph.graphql index d9d29019ee..d38a88745e 100644 --- a/apollo-router-benchmarks/benches/fixtures/supergraph.graphql +++ b/apollo-router-benchmarks/benches/fixtures/supergraph.graphql @@ -1,73 +1,128 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "http://localhost:4001/graphql") - INVENTORY @join__graph(name: "inventory" url: "http://localhost:4004/graphql") - PRODUCTS @join__graph(name: "products" url: "http://localhost:4003/graphql") - REVIEWS @join__graph(name: "reviews" url: "http://localhost:4002/graphql") + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) + recommendedProducts: [Product] @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router-scaffold/Cargo.toml b/apollo-router-scaffold/Cargo.toml index 4c47e46b94..4801a6c616 100644 --- a/apollo-router-scaffold/Cargo.toml +++ b/apollo-router-scaffold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-scaffold" -version = "1.54.0" +version = "1.55.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/scaffold-test/Cargo.toml b/apollo-router-scaffold/scaffold-test/Cargo.toml index 21bee82592..b2b03bbccd 100644 --- a/apollo-router-scaffold/scaffold-test/Cargo.toml +++ b/apollo-router-scaffold/scaffold-test/Cargo.toml @@ -17,7 +17,7 @@ schemars = "0.8.10" serde = "1.0.149" serde_json = "1.0.79" tokio = { version = "1.17.0", features = ["full"] } -tower = { version = "0.4.12", features = ["full"] } +tower = { version = "0.4.0", features = ["full"] } tracing = "0.1.37" # this makes build scripts and proc macros faster to compile diff --git a/apollo-router-scaffold/templates/base/Cargo.template.toml b/apollo-router-scaffold/templates/base/Cargo.template.toml index 290952c5a2..a1c3d2a42d 100644 --- a/apollo-router-scaffold/templates/base/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/Cargo.template.toml @@ -22,7 +22,7 @@ apollo-router = { path ="{{integration_test}}apollo-router" } apollo-router = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} # Note if you update these dependencies then also update xtask/Cargo.toml -apollo-router = "1.54.0" +apollo-router = "1.55.0" {{/if}} {{/if}} async-trait = "0.1.52" diff --git a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml index 9c9a956892..c566a3a0ca 100644 --- a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml @@ -13,7 +13,7 @@ apollo-router-scaffold = { path ="{{integration_test}}apollo-router-scaffold" } {{#if branch}} apollo-router-scaffold = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} -apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.54.0" } +apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.55.0" } {{/if}} {{/if}} anyhow = "1.0.58" diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index d34d25e39f..4675c9a5d7 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router" -version = "1.54.0" +version = "1.55.0" authors = ["Apollo Graph, Inc. "] repository = "https://github.com/apollographql/router/" documentation = "https://docs.rs/apollo-router" @@ -68,7 +68,7 @@ askama = "0.12.1" access-json = "0.1.0" anyhow = "1.0.86" apollo-compiler.workspace = true -apollo-federation = { path = "../apollo-federation", version = "=1.54.0" } +apollo-federation = { path = "../apollo-federation", version = "=1.55.0" } arc-swap = "1.6.0" async-channel = "1.9.0" async-compression = { version = "0.4.6", features = [ @@ -79,7 +79,7 @@ async-compression = { version = "0.4.6", features = [ ] } async-trait.workspace = true axum = { version = "0.6.20", features = ["headers", "json", "original-uri"] } -base64 = "0.21.7" +base64 = "0.22.0" bloomfilter = "1.0.13" buildstructor = "0.5.4" bytes = "1.6.0" @@ -106,17 +106,17 @@ displaydoc = "0.2" flate2 = "1.0.30" fred = { version = "7.1.2", features = ["enable-rustls"] } futures = { version = "0.3.30", features = ["thread-pool"] } -graphql_client = "0.13.0" +graphql_client = "0.14.0" hex.workspace = true http.workspace = true http-body = "0.4.6" -heck = "0.4.1" +heck = "0.5.0" humantime = "2.1.0" humantime-serde = "1.1.1" hyper = { version = "0.14.28", features = ["server", "client", "stream"] } hyper-rustls = { version = "0.24.2", features = ["http1", "http2"] } indexmap = { version = "2.2.6", features = ["serde"] } -itertools = "0.12.1" +itertools = "0.13.0" jsonpath_lib = "0.3.0" jsonpath-rust = "0.3.5" jsonschema = { version = "0.17.1", default-features = false } @@ -127,15 +127,15 @@ linkme = "0.3.27" lru = "0.12.3" maplit = "1.0.2" mediatype = "0.19.18" -mockall = "0.11.4" +mockall = "0.13.0" mime = "0.3.17" multer = "2.1.0" -multimap = "0.9.1" +multimap = "0.9.1" # Warning: part of the public API # To avoid tokio issues notify = { version = "6.1.1", default-features = false, features = [ "macos_kqueue", ] } -nu-ansi-term = "0.49" +nu-ansi-term = "0.50" num-traits = "0.2.19" once_cell = "1.19.0" @@ -197,7 +197,7 @@ regex = "1.10.5" reqwest.workspace = true # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.6.1+v2.9.0" +router-bridge = "=0.6.2+v2.9.1" rust-embed = { version = "8.4.0", features = ["include-exclude"] } rustls = "0.21.12" @@ -214,7 +214,7 @@ serde_json.workspace = true serde_urlencoded = "0.7.1" serde_yaml = "0.8.26" static_assertions = "1.1.0" -strum_macros = "0.25.3" +strum_macros = "0.26.0" sys-info = "0.9.1" thiserror = "1.0.61" tokio.workspace = true @@ -227,7 +227,7 @@ tonic = { version = "0.9.2", features = [ "gzip", ] } tower.workspace = true -tower-http = { version = "0.4.4", features = [ +tower-http = { version = "0.4.0", features = [ "add-extension", "trace", "cors", @@ -268,9 +268,9 @@ aws-credential-types = "1.1.6" aws-config = "1.1.6" aws-types = "1.1.6" aws-smithy-runtime-api = { version = "1.1.6", features = ["client"] } -aws-sdk-sso = "=1.39.0" # TODO: unpin when on Rust 1.78+ -aws-sdk-ssooidc = "=1.40.0" # TODO: unpin when on Rust 1.78+ -aws-sdk-sts = "=1.39.0" # TODO: unpin when on Rust 1.78+ +aws-sdk-sso = "=1.39.0" # TODO: unpin when on Rust 1.78+ +aws-sdk-ssooidc = "=1.40.0" # TODO: unpin when on Rust 1.78+ +aws-sdk-sts = "=1.39.0" # TODO: unpin when on Rust 1.78+ sha1.workspace = true tracing-serde = "0.1.3" time = { version = "0.3.36", features = ["serde"] } @@ -291,7 +291,7 @@ hyperlocal = { version = "0.8.0", default-features = false, features = [ ] } [target.'cfg(target_os = "linux")'.dependencies] -tikv-jemallocator = "0.5.4" +tikv-jemallocator = "0.6.0" [dev-dependencies] axum = { version = "0.6.20", features = [ @@ -306,7 +306,7 @@ futures-test = "0.3.30" insta.workspace = true maplit = "1.0.2" memchr = { version = "2.7.4", default-features = false } -mockall = "0.11.4" +mockall = "0.13.0" num-traits = "0.2.19" once_cell.workspace = true opentelemetry-stdout = { version = "0.1.0", features = ["trace"] } @@ -320,7 +320,7 @@ opentelemetry-proto = { version = "0.5.0", features = [ opentelemetry-datadog = { version = "0.8.0", features = ["reqwest-client"] } p256 = "0.13.2" rand_core = "0.6.4" -reqwest = { version = "0.11.27", default-features = false, features = [ +reqwest = { version = "0.11.0", default-features = false, features = [ "json", "multipart", "stream", @@ -336,7 +336,6 @@ tempfile.workspace = true test-log = { version = "0.2.16", default-features = false, features = [ "trace", ] } -test-span = "0.7.0" basic-toml = "0.1.9" tower-test = "0.4.0" diff --git a/apollo-router/benches/deeply_nested/supergraph.graphql b/apollo-router/benches/deeply_nested/supergraph.graphql index e28b86e773..bf51cbf7ba 100644 --- a/apollo-router/benches/deeply_nested/supergraph.graphql +++ b/apollo-router/benches/deeply_nested/supergraph.graphql @@ -1,33 +1,60 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE directive @join__field( graph: join__Graph requires: join__FieldSet provides: join__FieldSet -) on FIELD_DEFINITION + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE directive @join__type( graph: join__Graph! key: join__FieldSet -) repeatable on OBJECT | INTERFACE + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet +scalar link__Import enum join__Graph { SUBGRAPH_1 @join__graph(name: "subgraph_1", url: "http://127.0.0.1:44168/") } +enum link__Purpose { + SECURITY + EXECUTION +} + type Query { value: Int! next: Query diff --git a/apollo-router/benches/huge_requests/supergraph.graphql b/apollo-router/benches/huge_requests/supergraph.graphql index d5f3f94dba..a6ed81a5e3 100644 --- a/apollo-router/benches/huge_requests/supergraph.graphql +++ b/apollo-router/benches/huge_requests/supergraph.graphql @@ -1,34 +1,61 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE directive @join__field( graph: join__Graph requires: join__FieldSet provides: join__FieldSet -) on FIELD_DEFINITION + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE directive @join__type( graph: join__Graph! key: join__FieldSet -) repeatable on OBJECT | INTERFACE + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet +scalar link__Import enum join__Graph { SUBGRAPH_1 @join__graph(name: "subgraph_1", url: "http://127.0.0.1:10141/") } +enum link__Purpose { + SECURITY + EXECUTION +} + type Query { unused: Int } diff --git a/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap b/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap index 6d6e785101..c08d3d1693 100644 --- a/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap +++ b/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__tests__defer_is_not_buffered.snap @@ -20,7 +20,7 @@ expression: parts }, "errors": [ { - "message": "couldn't find mock for query {\"query\":\"query TopProducts__reviews__1($representations:[_Any!]!){_entities(representations:$representations){...on Product{reviews{__typename id product{__typename upc}}}}}\",\"operationName\":\"TopProducts__reviews__1\",\"variables\":{\"representations\":[{\"__typename\":\"Product\",\"upc\":\"1\"},{\"__typename\":\"Product\",\"upc\":\"2\"}]}}", + "message": "couldn't find mock for query {\"query\":\"query($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { reviews { __typename id product { __typename upc } } } } }\",\"variables\":{\"representations\":[{\"__typename\":\"Product\",\"upc\":\"1\"},{\"__typename\":\"Product\",\"upc\":\"2\"}]}}", "path": [ "topProducts", "@" diff --git a/apollo-router/src/axum_factory/tests.rs b/apollo-router/src/axum_factory/tests.rs index 703193683a..87198b8f4f 100644 --- a/apollo-router/src/axum_factory/tests.rs +++ b/apollo-router/src/axum_factory/tests.rs @@ -2267,10 +2267,14 @@ async fn test_supergraph_timeout() { let schema = include_str!("..//testdata/minimal_supergraph.graphql"); let schema = Arc::new(Schema::parse(schema, &conf).unwrap()); - let planner = - BridgeQueryPlannerPool::new(schema.clone(), conf.clone(), NonZeroUsize::new(1).unwrap()) - .await - .unwrap(); + let planner = BridgeQueryPlannerPool::new( + Vec::new(), + schema.clone(), + conf.clone(), + NonZeroUsize::new(1).unwrap(), + ) + .await + .unwrap(); // we do the entire supergraph rebuilding instead of using `from_supergraph_mock_callback_and_configuration` // because we need the plugins to apply on the supergraph diff --git a/apollo-router/src/cache/mod.rs b/apollo-router/src/cache/mod.rs index 6e1ef01cb8..04b578a437 100644 --- a/apollo-router/src/cache/mod.rs +++ b/apollo-router/src/cache/mod.rs @@ -137,6 +137,10 @@ where pub(crate) fn in_memory_cache(&self) -> InMemoryCache { self.storage.in_memory_cache() } + + pub(crate) fn activate(&self) { + self.storage.activate() + } } pub(crate) struct Entry { diff --git a/apollo-router/src/cache/redis.rs b/apollo-router/src/cache/redis.rs index f16130c116..c8d7b10d07 100644 --- a/apollo-router/src/cache/redis.rs +++ b/apollo-router/src/cache/redis.rs @@ -12,6 +12,7 @@ use fred::prelude::KeysInterface; use fred::prelude::RedisClient; use fred::prelude::RedisError; use fred::prelude::RedisErrorKind; +use fred::prelude::RedisPool; use fred::types::ClusterRouting; use fred::types::Expiration; use fred::types::FromRedis; @@ -52,7 +53,7 @@ where #[derive(Clone)] pub(crate) struct RedisCacheStorage { - inner: Arc, + inner: Arc, namespace: Option>, pub(crate) ttl: Option, is_cluster: bool, @@ -168,47 +169,16 @@ impl RedisCacheStorage { }); } - let client = RedisClient::new( + Self::create_client( client_config, - Some(PerformanceConfig { - default_command_timeout: config.timeout.unwrap_or(Duration::from_millis(500)), - ..Default::default() - }), - None, - Some(ReconnectPolicy::new_exponential(0, 1, 2000, 5)), - ); - let _handle = client.connect(); - - // spawn tasks that listen for connection close or reconnect events - let mut error_rx = client.error_rx(); - let mut reconnect_rx = client.reconnect_rx(); - - tokio::spawn(async move { - while let Ok(error) = error_rx.recv().await { - tracing::error!("Client disconnected with error: {:?}", error); - } - }); - tokio::spawn(async move { - while reconnect_rx.recv().await.is_ok() { - tracing::info!("Redis client reconnected."); - } - }); - - // a TLS connection to a TCP Redis could hang, so we add a timeout - tokio::time::timeout(Duration::from_secs(5), client.wait_for_connect()) - .await - .map_err(|_| { - RedisError::new(RedisErrorKind::Timeout, "timeout connecting to Redis") - })??; - - tracing::trace!("redis connection established"); - Ok(Self { - inner: Arc::new(client), - namespace: config.namespace.map(Arc::new), - ttl: config.ttl, + config.timeout.unwrap_or(Duration::from_millis(500)), + config.pool_size as usize, + config.namespace, + config.ttl, + config.reset_ttl, is_cluster, - reset_ttl: config.reset_ttl, - }) + ) + .await } #[cfg(test)] @@ -218,34 +188,58 @@ impl RedisCacheStorage { ..Default::default() }; - let client = RedisClient::new( + Self::create_client( + client_config, + Duration::from_millis(2), + 1, + None, + None, + false, + false, + ) + .await + } + + async fn create_client( + client_config: RedisConfig, + timeout: Duration, + pool_size: usize, + namespace: Option, + ttl: Option, + reset_ttl: bool, + is_cluster: bool, + ) -> Result { + let pooled_client = RedisPool::new( client_config, Some(PerformanceConfig { - default_command_timeout: Duration::from_millis(2), + default_command_timeout: timeout, ..Default::default() }), None, Some(ReconnectPolicy::new_exponential(0, 1, 2000, 5)), - ); - let _handle = client.connect(); - - // spawn tasks that listen for connection close or reconnect events - let mut error_rx = client.error_rx(); - let mut reconnect_rx = client.reconnect_rx(); - - tokio::spawn(async move { - while let Ok(error) = error_rx.recv().await { - tracing::error!("Client disconnected with error: {:?}", error); - } - }); - tokio::spawn(async move { - while reconnect_rx.recv().await.is_ok() { - tracing::info!("Redis client reconnected."); - } - }); + pool_size, + )?; + let _handle = pooled_client.connect(); + + for client in pooled_client.clients() { + // spawn tasks that listen for connection close or reconnect events + let mut error_rx = client.error_rx(); + let mut reconnect_rx = client.reconnect_rx(); + + tokio::spawn(async move { + while let Ok(error) = error_rx.recv().await { + tracing::error!("Client disconnected with error: {:?}", error); + } + }); + tokio::spawn(async move { + while reconnect_rx.recv().await.is_ok() { + tracing::info!("Redis client reconnected."); + } + }); + } // a TLS connection to a TCP Redis could hang, so we add a timeout - tokio::time::timeout(Duration::from_secs(5), client.wait_for_connect()) + tokio::time::timeout(Duration::from_secs(5), pooled_client.wait_for_connect()) .await .map_err(|_| { RedisError::new(RedisErrorKind::Timeout, "timeout connecting to Redis") @@ -253,11 +247,11 @@ impl RedisCacheStorage { tracing::trace!("redis connection established"); Ok(Self { - inner: Arc::new(client), - ttl: None, - namespace: None, - is_cluster: false, - reset_ttl: false, + inner: Arc::new(pooled_client), + namespace: namespace.map(Arc::new), + ttl, + is_cluster, + reset_ttl, }) } @@ -370,7 +364,7 @@ impl RedisCacheStorage { key: RedisKey, ) -> Option> { if self.reset_ttl && self.ttl.is_some() { - let pipeline: fred::clients::Pipeline = self.inner.pipeline(); + let pipeline: fred::clients::Pipeline = self.inner.next().pipeline(); let key = self.make_key(key); let res = pipeline .get::(&key) @@ -541,7 +535,7 @@ impl RedisCacheStorage { None => self.inner.mset(data.to_owned()).await, Some(ttl) => { let expiration = Some(Expiration::EX(ttl.as_secs() as i64)); - let pipeline = self.inner.pipeline(); + let pipeline = self.inner.next().pipeline(); for (key, value) in data { let _ = pipeline @@ -593,9 +587,9 @@ impl RedisCacheStorage { count: Option, ) -> Pin> + Send>> { if self.is_cluster { - Box::pin(self.inner.scan_cluster(pattern, count, None)) + Box::pin(self.inner.next().scan_cluster(pattern, count, None)) } else { - Box::pin(self.inner.scan(pattern, count, None)) + Box::pin(self.inner.next().scan(pattern, count, None)) } } } diff --git a/apollo-router/src/cache/storage.rs b/apollo-router/src/cache/storage.rs index 7cfa37a0ad..15f3b3dc8a 100644 --- a/apollo-router/src/cache/storage.rs +++ b/apollo-router/src/cache/storage.rs @@ -8,7 +8,6 @@ use std::sync::Arc; use lru::LruCache; use opentelemetry::metrics::MeterProvider; -use opentelemetry_api::metrics::Meter; use opentelemetry_api::metrics::ObservableGauge; use opentelemetry_api::metrics::Unit; use opentelemetry_api::KeyValue; @@ -53,13 +52,14 @@ pub(crate) type InMemoryCache = Arc>>; // a suitable implementation. #[derive(Clone)] pub(crate) struct CacheStorage { - caller: String, + caller: &'static str, inner: Arc>>, redis: Option, cache_size: Arc, cache_estimated_storage: Arc, - _cache_size_gauge: ObservableGauge, - _cache_estimated_storage_gauge: ObservableGauge, + // It's OK for these to be mutexes as they are only initialized once + cache_size_gauge: Arc>>>, + cache_estimated_storage_gauge: Arc>>>, } impl CacheStorage @@ -72,18 +72,12 @@ where config: Option, caller: &'static str, ) -> Result { - // Because calculating the cache size is expensive we do this as we go rather than iterating. This means storing the values for the gauges - let meter: opentelemetry::metrics::Meter = metrics::meter_provider().meter(METER_NAME); - let (cache_size, cache_size_gauge) = Self::create_cache_size_gauge(&meter, caller); - let (cache_estimated_storage, cache_estimated_storage_gauge) = - Self::create_cache_estimated_storage_size_gauge(&meter, caller); - Ok(Self { - _cache_size_gauge: cache_size_gauge, - _cache_estimated_storage_gauge: cache_estimated_storage_gauge, - cache_size, - cache_estimated_storage, - caller: caller.to_string(), + cache_size_gauge: Default::default(), + cache_estimated_storage_gauge: Default::default(), + cache_size: Default::default(), + cache_estimated_storage: Default::default(), + caller, inner: Arc::new(Mutex::new(LruCache::new(max_capacity))), redis: if let Some(config) = config { let required_to_start = config.required_to_start; @@ -107,13 +101,11 @@ where }) } - fn create_cache_size_gauge( - meter: &Meter, - caller: &'static str, - ) -> (Arc, ObservableGauge) { - let current_cache_size = Arc::new(AtomicI64::new(0)); - let current_cache_size_for_gauge = current_cache_size.clone(); - let cache_size_gauge = meter + fn create_cache_size_gauge(&self) -> ObservableGauge { + let meter: opentelemetry::metrics::Meter = metrics::meter_provider().meter(METER_NAME); + let current_cache_size_for_gauge = self.cache_size.clone(); + let caller = self.caller; + meter // TODO move to dot naming convention .i64_observable_gauge("apollo_router_cache_size") .with_description("Cache size") @@ -126,16 +118,13 @@ where ], ) }) - .init(); - (current_cache_size, cache_size_gauge) + .init() } - fn create_cache_estimated_storage_size_gauge( - meter: &Meter, - caller: &'static str, - ) -> (Arc, ObservableGauge) { - let cache_estimated_storage = Arc::new(AtomicI64::new(0)); - let cache_estimated_storage_for_gauge = cache_estimated_storage.clone(); + fn create_cache_estimated_storage_size_gauge(&self) -> ObservableGauge { + let meter: opentelemetry::metrics::Meter = metrics::meter_provider().meter(METER_NAME); + let cache_estimated_storage_for_gauge = self.cache_estimated_storage.clone(); + let caller = self.caller; let cache_estimated_storage_gauge = meter .i64_observable_gauge("apollo.router.cache.storage.estimated_size") .with_description("Estimated cache storage") @@ -154,7 +143,7 @@ where } }) .init(); - (cache_estimated_storage, cache_estimated_storage_gauge) + cache_estimated_storage_gauge } /// `init_from_redis` is called with values newly deserialized from Redis cache @@ -292,6 +281,17 @@ where pub(crate) async fn len(&self) -> usize { self.inner.lock().await.len() } + + pub(crate) fn activate(&self) { + // Gauges MUST be created after the meter provider is initialized + // This means that on reload we need a non-fallible way to recreate the gauges, hence this function. + *self.cache_size_gauge.lock().expect("lock poisoned") = + Some(self.create_cache_size_gauge()); + *self + .cache_estimated_storage_gauge + .lock() + .expect("lock poisoned") = Some(self.create_cache_estimated_storage_size_gauge()); + } } enum CacheStorageName { @@ -350,6 +350,7 @@ mod test { CacheStorage::new(NonZeroUsize::new(10).unwrap(), None, "test") .await .unwrap(); + cache.activate(); cache.insert("test".to_string(), Stuff {}).await; assert_gauge!( @@ -385,6 +386,7 @@ mod test { CacheStorage::new(NonZeroUsize::new(10).unwrap(), None, "test") .await .unwrap(); + cache.activate(); cache.insert("test".to_string(), Stuff {}).await; // This metric won't exist @@ -418,6 +420,7 @@ mod test { CacheStorage::new(NonZeroUsize::new(1).unwrap(), None, "test") .await .unwrap(); + cache.activate(); cache .insert( diff --git a/apollo-router/src/configuration/migrations/0028-entity_cache_type_metrics.yaml b/apollo-router/src/configuration/migrations/0028-entity_cache_type_metrics.yaml new file mode 100644 index 0000000000..e55c9ea6d4 --- /dev/null +++ b/apollo-router/src/configuration/migrations/0028-entity_cache_type_metrics.yaml @@ -0,0 +1,5 @@ +description: Entity cache entity.type attribute renamed to graphql.type.name +actions: + - type: move + from: telemetry.instrumentation.instruments.cache["apollo.router.operations.entity.cache"].attributes["entity.type"] + to: telemetry.instrumentation.instruments.cache["apollo.router.operations.entity.cache"].attributes["graphql.type.name"] \ No newline at end of file diff --git a/apollo-router/src/configuration/migrations/0029-pq-prewarm-to-on_startup.yaml b/apollo-router/src/configuration/migrations/0029-pq-prewarm-to-on_startup.yaml new file mode 100644 index 0000000000..0c6c755711 --- /dev/null +++ b/apollo-router/src/configuration/migrations/0029-pq-prewarm-to-on_startup.yaml @@ -0,0 +1,12 @@ +description: Experimental persisted query prewarm query plan cache can now be configured separately for `on_startup` and `on_reload`; the previous value now means `on_startup` +actions: + - type: change + path: persisted_queries.experimental_prewarm_query_plan_cache + from: true + to: + on_startup: true + - type: change + path: persisted_queries.experimental_prewarm_query_plan_cache + from: false + to: + on_startup: false diff --git a/apollo-router/src/configuration/mod.rs b/apollo-router/src/configuration/mod.rs index 8f467b9be5..83d0e44a1e 100644 --- a/apollo-router/src/configuration/mod.rs +++ b/apollo-router/src/configuration/mod.rs @@ -15,6 +15,7 @@ use displaydoc::Display; use itertools::Itertools; use once_cell::sync::Lazy; pub(crate) use persisted_queries::PersistedQueries; +pub(crate) use persisted_queries::PersistedQueriesPrewarmQueryPlanCache; #[cfg(test)] pub(crate) use persisted_queries::PersistedQueriesSafelist; use regex::Regex; @@ -238,10 +239,10 @@ pub(crate) enum IntrospectionMode { /// Use the new Rust-based implementation. New, /// Use the old JavaScript-based implementation. - #[default] Legacy, /// Use Rust-based and Javascript-based implementations side by side, /// logging warnings if the implementations disagree. + #[default] Both, } @@ -1010,6 +1011,10 @@ pub(crate) struct QueryPlanRedisCache { #[serde(default = "default_reset_ttl")] /// When a TTL is set on a key, reset it when reading the data from that key pub(crate) reset_ttl: bool, + + #[serde(default = "default_query_planner_cache_pool_size")] + /// The size of the Redis connection pool + pub(crate) pool_size: u32, } fn default_query_plan_cache_ttl() -> Duration { @@ -1017,6 +1022,10 @@ fn default_query_plan_cache_ttl() -> Duration { Duration::from_secs(86400 * 30) } +fn default_query_planner_cache_pool_size() -> u32 { + 1 +} + /// Cache configuration #[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields, default)] @@ -1088,12 +1097,20 @@ pub(crate) struct RedisCache { #[serde(default = "default_reset_ttl")] /// When a TTL is set on a key, reset it when reading the data from that key pub(crate) reset_ttl: bool, + + #[serde(default = "default_pool_size")] + /// The size of the Redis connection pool + pub(crate) pool_size: u32, } fn default_required_to_start() -> bool { false } +fn default_pool_size() -> u32 { + 1 +} + impl From for RedisCache { fn from(value: QueryPlanRedisCache) -> Self { RedisCache { @@ -1106,6 +1123,7 @@ impl From for RedisCache { tls: value.tls, required_to_start: value.required_to_start, reset_ttl: value.reset_ttl, + pool_size: value.pool_size, } } } diff --git a/apollo-router/src/configuration/persisted_queries.rs b/apollo-router/src/configuration/persisted_queries.rs index e1ae76e0e0..2b395b79ff 100644 --- a/apollo-router/src/configuration/persisted_queries.rs +++ b/apollo-router/src/configuration/persisted_queries.rs @@ -16,7 +16,7 @@ pub struct PersistedQueries { pub safelist: PersistedQueriesSafelist, /// Experimental feature to prewarm the query plan cache with persisted queries - pub experimental_prewarm_query_plan_cache: bool, + pub experimental_prewarm_query_plan_cache: PersistedQueriesPrewarmQueryPlanCache, /// Enables using a local copy of the persisted query manifest to safelist operations pub experimental_local_manifests: Option>, @@ -30,7 +30,7 @@ impl PersistedQueries { enabled: Option, log_unknown: Option, safelist: Option, - experimental_prewarm_query_plan_cache: Option, + experimental_prewarm_query_plan_cache: Option, experimental_local_manifests: Option>, ) -> Self { Self { @@ -38,7 +38,7 @@ impl PersistedQueries { safelist: safelist.unwrap_or_default(), log_unknown: log_unknown.unwrap_or_else(default_log_unknown), experimental_prewarm_query_plan_cache: experimental_prewarm_query_plan_cache - .unwrap_or_else(default_prewarm_query_plan_cache), + .unwrap_or_default(), experimental_local_manifests, } } @@ -67,13 +67,24 @@ impl PersistedQueriesSafelist { } } +/// Persisted Queries (PQ) query plan cache prewarm configuration +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields, default)] +pub struct PersistedQueriesPrewarmQueryPlanCache { + /// Enabling this field uses the persisted query list to pre-warm the query planner cache on startup (disabled by default) + pub on_startup: bool, + + /// Enabling this field uses the persisted query list to pre-warm the query planner cache on schema and config changes (enabled by default) + pub on_reload: bool, +} + impl Default for PersistedQueries { fn default() -> Self { Self { enabled: default_pq(), safelist: PersistedQueriesSafelist::default(), log_unknown: default_log_unknown(), - experimental_prewarm_query_plan_cache: default_prewarm_query_plan_cache(), + experimental_prewarm_query_plan_cache: PersistedQueriesPrewarmQueryPlanCache::default(), experimental_local_manifests: None, } } @@ -88,6 +99,15 @@ impl Default for PersistedQueriesSafelist { } } +impl Default for PersistedQueriesPrewarmQueryPlanCache { + fn default() -> Self { + Self { + on_startup: false, + on_reload: true, + } + } +} + const fn default_pq() -> bool { false } @@ -103,7 +123,3 @@ const fn default_require_id() -> bool { const fn default_log_unknown() -> bool { false } - -const fn default_prewarm_query_plan_cache() -> bool { - false -} diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 2a143d67b6..4eba0206d0 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -470,11 +470,10 @@ expression: "&schema" "CacheAttributes": { "additionalProperties": false, "properties": { - "entity.type": { - "default": null, - "description": "Entity type", - "nullable": true, - "type": "boolean" + "graphql.type.name": { + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -2787,34 +2786,29 @@ expression: "&schema" "additionalProperties": false, "properties": { "graphql.field.name": { - "default": null, - "description": "The GraphQL field name", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.field.type": { - "default": null, - "description": "The GraphQL field type", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.list.length": { - "default": null, - "description": "If the field is a list, the length of the list", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.name": { - "default": null, - "description": "The GraphQL operation name", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.type.name": { - "default": null, - "description": "The GraphQL type name", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -3597,6 +3591,13 @@ expression: "&schema" "InvalidationEndpointConfig": { "additionalProperties": false, "properties": { + "concurrent_requests": { + "default": 10, + "description": "Number of concurrent invalidation requests", + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, "listen": { "$ref": "#/definitions/ListenAddr", "description": "#/definitions/ListenAddr" @@ -3604,6 +3605,13 @@ expression: "&schema" "path": { "description": "Specify on which path you want to listen for invalidation endpoint.", "type": "string" + }, + "scan_count": { + "default": 1000, + "description": "Number of keys to return at once from a redis SCAN command", + "format": "uint32", + "minimum": 0.0, + "type": "integer" } }, "required": [ @@ -4111,9 +4119,8 @@ expression: "&schema" "type": "array" }, "experimental_prewarm_query_plan_cache": { - "default": false, - "description": "Experimental feature to prewarm the query plan cache with persisted queries", - "type": "boolean" + "$ref": "#/definitions/PersistedQueriesPrewarmQueryPlanCache", + "description": "#/definitions/PersistedQueriesPrewarmQueryPlanCache" }, "log_unknown": { "default": false, @@ -4127,6 +4134,23 @@ expression: "&schema" }, "type": "object" }, + "PersistedQueriesPrewarmQueryPlanCache": { + "additionalProperties": false, + "description": "Persisted Queries (PQ) query plan cache prewarm configuration", + "properties": { + "on_reload": { + "default": true, + "description": "Enabling this field uses the persisted query list to pre-warm the query planner cache on schema and config changes (enabled by default)", + "type": "boolean" + }, + "on_startup": { + "default": false, + "description": "Enabling this field uses the persisted query list to pre-warm the query planner cache on startup (disabled by default)", + "type": "boolean" + } + }, + "type": "object" + }, "PersistedQueriesSafelist": { "additionalProperties": false, "description": "Persisted Queries (PQ) Safelisting configuration", @@ -4333,6 +4357,13 @@ expression: "&schema" "nullable": true, "type": "string" }, + "pool_size": { + "default": 1, + "description": "The size of the Redis connection pool", + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, "required_to_start": { "default": false, "description": "Prevents the router from starting if it cannot connect to Redis", @@ -4543,6 +4574,13 @@ expression: "&schema" "nullable": true, "type": "string" }, + "pool_size": { + "default": 1, + "description": "The size of the Redis connection pool", + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, "required_to_start": { "default": false, "description": "Prevents the router from starting if it cannot connect to Redis", @@ -4709,136 +4747,114 @@ expression: "&schema" "type": "boolean" }, "dd.trace_id": { - "default": null, - "description": "The datadog trace ID. This can be output in logs and used to correlate traces in Datadog.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "error.type": { - "default": null, - "description": "Describes a class of error the operation ended with. Examples:\n\n* timeout * name_resolution_error * 500\n\nRequirement level: Conditionally Required: If request has ended with an error.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.request.body.size": { - "default": null, - "description": "The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples:\n\n* 3495\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.request.method": { - "default": null, - "description": "HTTP request method. Examples:\n\n* GET * POST * HEAD\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.response.body.size": { - "default": null, - "description": "The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples:\n\n* 3495\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.response.status_code": { - "default": null, - "description": "HTTP response status code. Examples:\n\n* 200\n\nRequirement level: Conditionally Required: If and only if one was received/sent.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.route": { - "default": null, - "description": "The matched route (path template in the format used by the respective server framework). Examples:\n\n* /graphql\n\nRequirement level: Conditionally Required: If and only if it’s available", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.local.address": { - "default": null, - "description": "Local socket address. Useful in case of a multi-IP host. Examples:\n\n* 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Opt-In", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.local.port": { - "default": null, - "description": "Local socket port. Useful in case of a multi-port host. Examples:\n\n* 65123\n\nRequirement level: Opt-In", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.peer.address": { - "default": null, - "description": "Peer address of the network connection - IP address or Unix domain socket name. Examples:\n\n* 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.peer.port": { - "default": null, - "description": "Peer port number of the network connection. Examples:\n\n* 65123\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.protocol.name": { - "default": null, - "description": "OSI application layer or non-OSI equivalent. Examples:\n\n* http * spdy\n\nRequirement level: Recommended: if not default (http).", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.protocol.version": { - "default": null, - "description": "Version of the protocol specified in network.protocol.name. Examples:\n\n* 1.0 * 1.1 * 2 * 3\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.transport": { - "default": null, - "description": "OSI transport layer. Examples:\n\n* tcp * udp\n\nRequirement level: Conditionally Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.type": { - "default": null, - "description": "OSI network layer or non-OSI equivalent. Examples:\n\n* ipv4 * ipv6\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "server.address": { - "default": null, - "description": "Name of the local HTTP server that received the request. Examples:\n\n* example.com * 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "server.port": { - "default": null, - "description": "Port of the local HTTP server that received the request. Examples:\n\n* 80 * 8080 * 443\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "trace_id": { - "default": null, - "description": "The OpenTelemetry trace ID. This can be output in logs.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.path": { - "default": null, - "description": "The URI path component Examples:\n\n* /search\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.query": { - "default": null, - "description": "The URI query component Examples:\n\n* q=OpenTelemetry\n\nRequirement level: Conditionally Required: If and only if one was received/sent.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.scheme": { - "default": null, - "description": "The URI scheme component identifying the used protocol. Examples:\n\n* http * https\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "user_agent.original": { - "default": null, - "description": "Value of the HTTP User-Agent header sent by the client. Examples:\n\n* CERN-LineMode/2.15 * libwww/2.17b3\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -5438,6 +5454,25 @@ expression: "&schema" ], "type": "string" }, + "StandardAttribute": { + "anyOf": [ + { + "type": "boolean" + }, + { + "additionalProperties": false, + "properties": { + "alias": { + "type": "string" + } + }, + "required": [ + "alias" + ], + "type": "object" + } + ] + }, "StandardEventConfig_for_RouterSelector": { "anyOf": [ { @@ -5655,28 +5690,24 @@ expression: "&schema" "additionalProperties": false, "properties": { "subgraph.graphql.document": { - "default": null, - "description": "The GraphQL document being executed. Examples:\n\n* `query findBookById { bookById(id: ?) { name } }`\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.graphql.operation.name": { - "default": null, - "description": "The name of the operation being executed. Examples:\n\n* findBookById\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.graphql.operation.type": { - "default": null, - "description": "The type of the operation being executed. Examples:\n\n* query * subscription * mutation\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.name": { - "default": null, - "description": "The name of the subgraph Examples:\n\n* products\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -6556,46 +6587,39 @@ expression: "&schema" "description": "Attributes for Cost", "properties": { "cost.actual": { - "default": null, - "description": "The actual cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.delta": { - "default": null, - "description": "The delta (estimated - actual) cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.estimated": { - "default": null, - "description": "The estimated cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.result": { - "default": null, - "description": "The cost result, this is an error code returned by the cost calculation or COST_OK", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.document": { - "default": null, - "description": "The GraphQL document being executed. Examples:\n\n* `query findBookById { bookById(id: ?) { name } }`\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.name": { - "default": null, - "description": "The name of the operation being executed. Examples:\n\n* findBookById\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.type": { - "default": null, - "description": "The type of the operation being executed. Examples:\n\n* query * subscription * mutation\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -7418,136 +7442,114 @@ expression: "&schema" "type": "boolean" }, "dd.trace_id": { - "default": null, - "description": "The datadog trace ID. This can be output in logs and used to correlate traces in Datadog.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "error.type": { - "default": null, - "description": "Describes a class of error the operation ended with. Examples:\n\n* timeout * name_resolution_error * 500\n\nRequirement level: Conditionally Required: If request has ended with an error.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.request.body.size": { - "default": null, - "description": "The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples:\n\n* 3495\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.request.method": { - "default": null, - "description": "HTTP request method. Examples:\n\n* GET * POST * HEAD\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.response.body.size": { - "default": null, - "description": "The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples:\n\n* 3495\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.response.status_code": { - "default": null, - "description": "HTTP response status code. Examples:\n\n* 200\n\nRequirement level: Conditionally Required: If and only if one was received/sent.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.route": { - "default": null, - "description": "The matched route (path template in the format used by the respective server framework). Examples:\n\n* /graphql\n\nRequirement level: Conditionally Required: If and only if it’s available", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.local.address": { - "default": null, - "description": "Local socket address. Useful in case of a multi-IP host. Examples:\n\n* 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Opt-In", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.local.port": { - "default": null, - "description": "Local socket port. Useful in case of a multi-port host. Examples:\n\n* 65123\n\nRequirement level: Opt-In", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.peer.address": { - "default": null, - "description": "Peer address of the network connection - IP address or Unix domain socket name. Examples:\n\n* 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.peer.port": { - "default": null, - "description": "Peer port number of the network connection. Examples:\n\n* 65123\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.protocol.name": { - "default": null, - "description": "OSI application layer or non-OSI equivalent. Examples:\n\n* http * spdy\n\nRequirement level: Recommended: if not default (http).", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.protocol.version": { - "default": null, - "description": "Version of the protocol specified in network.protocol.name. Examples:\n\n* 1.0 * 1.1 * 2 * 3\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.transport": { - "default": null, - "description": "OSI transport layer. Examples:\n\n* tcp * udp\n\nRequirement level: Conditionally Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.type": { - "default": null, - "description": "OSI network layer or non-OSI equivalent. Examples:\n\n* ipv4 * ipv6\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "server.address": { - "default": null, - "description": "Name of the local HTTP server that received the request. Examples:\n\n* example.com * 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "server.port": { - "default": null, - "description": "Port of the local HTTP server that received the request. Examples:\n\n* 80 * 8080 * 443\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "trace_id": { - "default": null, - "description": "The OpenTelemetry trace ID. This can be output in logs.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.path": { - "default": null, - "description": "The URI path component Examples:\n\n* /search\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.query": { - "default": null, - "description": "The URI query component Examples:\n\n* q=OpenTelemetry\n\nRequirement level: Conditionally Required: If and only if one was received/sent.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.scheme": { - "default": null, - "description": "The URI scheme component identifying the used protocol. Examples:\n\n* http * https\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "user_agent.original": { - "default": null, - "description": "Value of the HTTP User-Agent header sent by the client. Examples:\n\n* CERN-LineMode/2.15 * libwww/2.17b3\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -7566,136 +7568,114 @@ expression: "&schema" "type": "boolean" }, "dd.trace_id": { - "default": null, - "description": "The datadog trace ID. This can be output in logs and used to correlate traces in Datadog.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "error.type": { - "default": null, - "description": "Describes a class of error the operation ended with. Examples:\n\n* timeout * name_resolution_error * 500\n\nRequirement level: Conditionally Required: If request has ended with an error.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.request.body.size": { - "default": null, - "description": "The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples:\n\n* 3495\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.request.method": { - "default": null, - "description": "HTTP request method. Examples:\n\n* GET * POST * HEAD\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.response.body.size": { - "default": null, - "description": "The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. Examples:\n\n* 3495\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.response.status_code": { - "default": null, - "description": "HTTP response status code. Examples:\n\n* 200\n\nRequirement level: Conditionally Required: If and only if one was received/sent.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "http.route": { - "default": null, - "description": "The matched route (path template in the format used by the respective server framework). Examples:\n\n* /graphql\n\nRequirement level: Conditionally Required: If and only if it’s available", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.local.address": { - "default": null, - "description": "Local socket address. Useful in case of a multi-IP host. Examples:\n\n* 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Opt-In", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.local.port": { - "default": null, - "description": "Local socket port. Useful in case of a multi-port host. Examples:\n\n* 65123\n\nRequirement level: Opt-In", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.peer.address": { - "default": null, - "description": "Peer address of the network connection - IP address or Unix domain socket name. Examples:\n\n* 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.peer.port": { - "default": null, - "description": "Peer port number of the network connection. Examples:\n\n* 65123\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.protocol.name": { - "default": null, - "description": "OSI application layer or non-OSI equivalent. Examples:\n\n* http * spdy\n\nRequirement level: Recommended: if not default (http).", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.protocol.version": { - "default": null, - "description": "Version of the protocol specified in network.protocol.name. Examples:\n\n* 1.0 * 1.1 * 2 * 3\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.transport": { - "default": null, - "description": "OSI transport layer. Examples:\n\n* tcp * udp\n\nRequirement level: Conditionally Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "network.type": { - "default": null, - "description": "OSI network layer or non-OSI equivalent. Examples:\n\n* ipv4 * ipv6\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "server.address": { - "default": null, - "description": "Name of the local HTTP server that received the request. Examples:\n\n* example.com * 10.1.2.80 * /tmp/my.sock\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "server.port": { - "default": null, - "description": "Port of the local HTTP server that received the request. Examples:\n\n* 80 * 8080 * 443\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "trace_id": { - "default": null, - "description": "The OpenTelemetry trace ID. This can be output in logs.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.path": { - "default": null, - "description": "The URI path component Examples:\n\n* /search\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.query": { - "default": null, - "description": "The URI query component Examples:\n\n* q=OpenTelemetry\n\nRequirement level: Conditionally Required: If and only if one was received/sent.", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "url.scheme": { - "default": null, - "description": "The URI scheme component identifying the used protocol. Examples:\n\n* http * https\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "user_agent.original": { - "default": null, - "description": "Value of the HTTP User-Agent header sent by the client. Examples:\n\n* CERN-LineMode/2.15 * libwww/2.17b3\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -7707,28 +7687,24 @@ expression: "&schema" }, "properties": { "subgraph.graphql.document": { - "default": null, - "description": "The GraphQL document being executed. Examples:\n\n* `query findBookById { bookById(id: ?) { name } }`\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.graphql.operation.name": { - "default": null, - "description": "The name of the operation being executed. Examples:\n\n* findBookById\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.graphql.operation.type": { - "default": null, - "description": "The type of the operation being executed. Examples:\n\n* query * subscription * mutation\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.name": { - "default": null, - "description": "The name of the subgraph Examples:\n\n* products\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -7740,28 +7716,24 @@ expression: "&schema" }, "properties": { "subgraph.graphql.document": { - "default": null, - "description": "The GraphQL document being executed. Examples:\n\n* `query findBookById { bookById(id: ?) { name } }`\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.graphql.operation.name": { - "default": null, - "description": "The name of the operation being executed. Examples:\n\n* findBookById\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.graphql.operation.type": { - "default": null, - "description": "The type of the operation being executed. Examples:\n\n* query * subscription * mutation\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "subgraph.name": { - "default": null, - "description": "The name of the subgraph Examples:\n\n* products\n\nRequirement level: Required", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -7774,46 +7746,39 @@ expression: "&schema" "description": "Attributes for Cost", "properties": { "cost.actual": { - "default": null, - "description": "The actual cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.delta": { - "default": null, - "description": "The delta (estimated - actual) cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.estimated": { - "default": null, - "description": "The estimated cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.result": { - "default": null, - "description": "The cost result, this is an error code returned by the cost calculation or COST_OK", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.document": { - "default": null, - "description": "The GraphQL document being executed. Examples:\n\n* `query findBookById { bookById(id: ?) { name } }`\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.name": { - "default": null, - "description": "The name of the operation being executed. Examples:\n\n* findBookById\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.type": { - "default": null, - "description": "The type of the operation being executed. Examples:\n\n* query * subscription * mutation\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -7826,46 +7791,39 @@ expression: "&schema" "description": "Attributes for Cost", "properties": { "cost.actual": { - "default": null, - "description": "The actual cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.delta": { - "default": null, - "description": "The delta (estimated - actual) cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.estimated": { - "default": null, - "description": "The estimated cost of the operation using the currently configured cost model", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "cost.result": { - "default": null, - "description": "The cost result, this is an error code returned by the cost calculation or COST_OK", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.document": { - "default": null, - "description": "The GraphQL document being executed. Examples:\n\n* `query findBookById { bookById(id: ?) { name } }`\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.name": { - "default": null, - "description": "The name of the operation being executed. Examples:\n\n* findBookById\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.type": { - "default": null, - "description": "The type of the operation being executed. Examples:\n\n* query * subscription * mutation\n\nRequirement level: Recommended", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -7889,11 +7847,10 @@ expression: "&schema" "description": "#/definitions/SubgraphSelector" }, "properties": { - "entity.type": { - "default": null, - "description": "Entity type", - "nullable": true, - "type": "boolean" + "graphql.type.name": { + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" @@ -7985,34 +7942,29 @@ expression: "&schema" }, "properties": { "graphql.field.name": { - "default": null, - "description": "The GraphQL field name", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.field.type": { - "default": null, - "description": "The GraphQL field type", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.list.length": { - "default": null, - "description": "If the field is a list, the length of the list", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.operation.name": { - "default": null, - "description": "The GraphQL operation name", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true }, "graphql.type.name": { - "default": null, - "description": "The GraphQL type name", - "nullable": true, - "type": "boolean" + "$ref": "#/definitions/StandardAttribute", + "description": "#/definitions/StandardAttribute", + "nullable": true } }, "type": "object" diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@datadog_enabled.router.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@datadog_enabled.router.yaml.snap index e7ada975e5..fff5223715 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@datadog_enabled.router.yaml.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@datadog_enabled.router.yaml.snap @@ -9,4 +9,3 @@ telemetry: datadog: endpoint: default enabled: true - diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-already-object.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-already-object.yaml.snap new file mode 100644 index 0000000000..8898a55cc9 --- /dev/null +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-already-object.yaml.snap @@ -0,0 +1,9 @@ +--- +source: apollo-router/src/configuration/tests.rs +expression: new_config +--- +--- +persisted_queries: + enabled: true + experimental_prewarm_query_plan_cache: + on_reload: false diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-false.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-false.yaml.snap new file mode 100644 index 0000000000..3d45bd15ac --- /dev/null +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-false.yaml.snap @@ -0,0 +1,9 @@ +--- +source: apollo-router/src/configuration/tests.rs +expression: new_config +--- +--- +persisted_queries: + enabled: true + experimental_prewarm_query_plan_cache: + on_startup: false diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-true.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-true.yaml.snap new file mode 100644 index 0000000000..ef598f91d2 --- /dev/null +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__upgrade_old_configuration@persisted-queries-prewarm-true.yaml.snap @@ -0,0 +1,9 @@ +--- +source: apollo-router/src/configuration/tests.rs +expression: new_config +--- +--- +persisted_queries: + enabled: true + experimental_prewarm_query_plan_cache: + on_startup: true diff --git a/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-already-object.yaml b/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-already-object.yaml new file mode 100644 index 0000000000..a6647b3a04 --- /dev/null +++ b/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-already-object.yaml @@ -0,0 +1,4 @@ +persisted_queries: + enabled: true + experimental_prewarm_query_plan_cache: + on_reload: false diff --git a/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-false.yaml b/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-false.yaml new file mode 100644 index 0000000000..704865131f --- /dev/null +++ b/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-false.yaml @@ -0,0 +1,3 @@ +persisted_queries: + enabled: true + experimental_prewarm_query_plan_cache: false diff --git a/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-true.yaml b/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-true.yaml new file mode 100644 index 0000000000..3a1fafa87d --- /dev/null +++ b/apollo-router/src/configuration/testdata/migrations/persisted-queries-prewarm-true.yaml @@ -0,0 +1,3 @@ +persisted_queries: + enabled: true + experimental_prewarm_query_plan_cache: true diff --git a/apollo-router/src/configuration/tests.rs b/apollo-router/src/configuration/tests.rs index d1288df215..fb7acabf04 100644 --- a/apollo-router/src/configuration/tests.rs +++ b/apollo-router/src/configuration/tests.rs @@ -35,28 +35,7 @@ fn schema_generation() { #[test] fn routing_url_in_schema() { - let schema = r#" - schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") - { - query: Query - } - - type Query { - me: String - } - - directive @core(feature: String!) repeatable on SCHEMA - - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "http://localhost:4001/graphql") - INVENTORY @join__graph(name: "inventory" url: "http://localhost:4002/graphql") - PRODUCTS @join__graph(name: "products" url: "http://localhost:4003/graphql") - REVIEWS @join__graph(name: "reviews" url: "http://localhost:4004/graphql") - } - "#; + let schema = include_str!("../testdata/minimal_local_inventory_supergraph.graphql"); let schema = crate::spec::Schema::parse(schema, &Default::default()).unwrap(); let subgraphs: HashMap<&str, &Uri> = schema.subgraphs().map(|(k, v)| (k.as_str(), v)).collect(); @@ -87,28 +66,8 @@ fn routing_url_in_schema() { #[test] fn missing_subgraph_url() { - let schema_error = r#" - schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") - { - query: Query - } - - type Query { - me: String - } - - directive @core(feature: String!) repeatable on SCHEMA - - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - - enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "http://localhost:4001/graphql") - INVENTORY @join__graph(name: "inventory" url: "http://localhost:4002/graphql") - PRODUCTS @join__graph(name: "products" url: "http://localhost:4003/graphql") - REVIEWS @join__graph(name: "reviews" url: "") - }"#; + let schema_error = + include_str!("../testdata/invalid_minimal_supergraph_missing_subgraph_url.graphql"); let schema_error = crate::spec::Schema::parse(schema_error, &Default::default()) .expect_err("Must have an error because we have one missing subgraph routing url"); diff --git a/apollo-router/src/configuration/upgrade.rs b/apollo-router/src/configuration/upgrade.rs index 0056801e86..d925ed7354 100644 --- a/apollo-router/src/configuration/upgrade.rs +++ b/apollo-router/src/configuration/upgrade.rs @@ -144,7 +144,7 @@ fn apply_migration(config: &Value, migration: &Migration) -> Result { - if !jsonpath_lib::select(config, &format!("$.{path} == {from}")) + if !jsonpath_lib::select(config, &format!("$[?(@.{path} == {from})]")) .unwrap_or_default() .is_empty() { diff --git a/apollo-router/src/context/mod.rs b/apollo-router/src/context/mod.rs index 73794512a0..daade4ea5a 100644 --- a/apollo-router/src/context/mod.rs +++ b/apollo-router/src/context/mod.rs @@ -429,27 +429,7 @@ mod test { #[test] fn test_executable_document_access() { let c = Context::new(); - let schema = r#" - schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") - { - query: Query - } - type Query { - me: String - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - - enum join__Graph { - ACCOUNTS @join__graph(name:"accounts" url: "http://localhost:4001/graphql") - INVENTORY - @join__graph(name: "inventory", url: "http://localhost:4004/graphql") - PRODUCTS - @join__graph(name: "products" url: "http://localhost:4003/graphql") - REVIEWS @join__graph(name: "reviews" url: "http://localhost:4002/graphql") - }"#; + let schema = include_str!("../testdata/minimal_supergraph.graphql"); let schema = Schema::parse(schema, &Default::default()).unwrap(); let document = Query::parse_document("{ me }", None, &schema, &Configuration::default()).unwrap(); diff --git a/apollo-router/src/graphql/visitor.rs b/apollo-router/src/graphql/visitor.rs index ee92b8741f..b344393821 100644 --- a/apollo-router/src/graphql/visitor.rs +++ b/apollo-router/src/graphql/visitor.rs @@ -1,13 +1,13 @@ -use serde_json_bytes::ByteString; -use serde_json_bytes::Map; use serde_json_bytes::Value; use crate::graphql::Response; +use crate::json_ext::Object; pub(crate) trait ResponseVisitor { fn visit_field( &mut self, request: &apollo_compiler::ExecutableDocument, + variables: &Object, _ty: &apollo_compiler::executable::NamedType, field: &apollo_compiler::executable::Field, value: &Value, @@ -15,11 +15,17 @@ pub(crate) trait ResponseVisitor { match value { Value::Array(items) => { for item in items { - self.visit_list_item(request, field.ty().inner_named_type(), field, item); + self.visit_list_item( + request, + variables, + field.ty().inner_named_type(), + field, + item, + ); } } Value::Object(children) => { - self.visit_selections(request, &field.selection_set, children); + self.visit_selections(request, variables, &field.selection_set, children); } _ => {} } @@ -28,6 +34,7 @@ pub(crate) trait ResponseVisitor { fn visit_list_item( &mut self, request: &apollo_compiler::ExecutableDocument, + variables: &Object, _ty: &apollo_compiler::executable::NamedType, field: &apollo_compiler::executable::Field, value: &Value, @@ -35,17 +42,22 @@ pub(crate) trait ResponseVisitor { match value { Value::Array(items) => { for item in items { - self.visit_list_item(request, _ty, field, item); + self.visit_list_item(request, variables, _ty, field, item); } } Value::Object(children) => { - self.visit_selections(request, &field.selection_set, children); + self.visit_selections(request, variables, &field.selection_set, children); } _ => {} } } - fn visit(&mut self, request: &apollo_compiler::ExecutableDocument, response: &Response) { + fn visit( + &mut self, + request: &apollo_compiler::ExecutableDocument, + response: &Response, + variables: &Object, + ) { if response.path.is_some() { // TODO: In this case, we need to find the selection inside `request` corresponding to the path so we can start zipping. // Exiting here means any implementing visitor will not operate on deffered responses. @@ -54,10 +66,10 @@ pub(crate) trait ResponseVisitor { if let Some(Value::Object(children)) = &response.data { if let Some(operation) = &request.operations.anonymous { - self.visit_selections(request, &operation.selection_set, children); + self.visit_selections(request, variables, &operation.selection_set, children); } for operation in request.operations.named.values() { - self.visit_selections(request, &operation.selection_set, children); + self.visit_selections(request, variables, &operation.selection_set, children); } } } @@ -65,21 +77,28 @@ pub(crate) trait ResponseVisitor { fn visit_selections( &mut self, request: &apollo_compiler::ExecutableDocument, + variables: &Object, selection_set: &apollo_compiler::executable::SelectionSet, - fields: &Map, + fields: &Object, ) { for selection in &selection_set.selections { match selection { apollo_compiler::executable::Selection::Field(inner_field) => { if let Some(value) = fields.get(inner_field.name.as_str()) { - self.visit_field(request, &selection_set.ty, inner_field.as_ref(), value); + self.visit_field( + request, + variables, + &selection_set.ty, + inner_field.as_ref(), + value, + ); } else { tracing::warn!("The response did not include a field corresponding to query field {:?}", inner_field); } } apollo_compiler::executable::Selection::FragmentSpread(fragment_spread) => { if let Some(fragment) = fragment_spread.fragment_def(request) { - self.visit_selections(request, &fragment.selection_set, fields); + self.visit_selections(request, variables, &fragment.selection_set, fields); } else { tracing::warn!( "The fragment {} was not found in the query document.", @@ -88,7 +107,12 @@ pub(crate) trait ResponseVisitor { } } apollo_compiler::executable::Selection::InlineFragment(inline_fragment) => { - self.visit_selections(request, &inline_fragment.selection_set, fields); + self.visit_selections( + request, + variables, + &inline_fragment.selection_set, + fields, + ); } } } @@ -121,7 +145,7 @@ mod tests { let response = Response::from_bytes("test", Bytes::from_static(response_bytes)).unwrap(); let mut visitor = FieldCounter::new(); - visitor.visit(&request, &response); + visitor.visit(&request, &response, &Default::default()); insta::with_settings!({sort_maps=>true}, { assert_yaml_snapshot!(visitor) }) } @@ -136,7 +160,7 @@ mod tests { let response = Response::from_bytes("test", Bytes::from_static(response_bytes)).unwrap(); let mut visitor = FieldCounter::new(); - visitor.visit(&request, &response); + visitor.visit(&request, &response, &Default::default()); insta::with_settings!({sort_maps=>true}, { assert_yaml_snapshot!(visitor) }) } @@ -151,7 +175,7 @@ mod tests { let response = Response::from_bytes("test", Bytes::from_static(response_bytes)).unwrap(); let mut visitor = FieldCounter::new(); - visitor.visit(&request, &response); + visitor.visit(&request, &response, &Default::default()); insta::with_settings!({sort_maps=>true}, { assert_yaml_snapshot!(visitor) }) } @@ -166,7 +190,7 @@ mod tests { let response = Response::from_bytes("test", Bytes::from_static(response_bytes)).unwrap(); let mut visitor = FieldCounter::new(); - visitor.visit(&request, &response); + visitor.visit(&request, &response, &Default::default()); insta::with_settings!({sort_maps=>true}, { assert_yaml_snapshot!(visitor) }) } @@ -186,6 +210,7 @@ mod tests { fn visit_field( &mut self, request: &ExecutableDocument, + variables: &Object, _ty: &apollo_compiler::executable::NamedType, field: &apollo_compiler::executable::Field, value: &Value, @@ -195,11 +220,17 @@ mod tests { match value { Value::Array(items) => { for item in items { - self.visit_list_item(request, field.ty().inner_named_type(), field, item); + self.visit_list_item( + request, + variables, + field.ty().inner_named_type(), + field, + item, + ); } } Value::Object(children) => { - self.visit_selections(request, &field.selection_set, children); + self.visit_selections(request, variables, &field.selection_set, children); } _ => {} } diff --git a/apollo-router/src/introspection.rs b/apollo-router/src/introspection.rs index 597eb4fa06..be0e4a1d88 100644 --- a/apollo-router/src/introspection.rs +++ b/apollo-router/src/introspection.rs @@ -14,25 +14,25 @@ use crate::query_planner::QueryPlanResult; const DEFAULT_INTROSPECTION_CACHE_CAPACITY: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(5) }; +pub(crate) async fn default_cache_storage() -> CacheStorage { + // This cannot fail as redis is not used. + CacheStorage::new(DEFAULT_INTROSPECTION_CACHE_CAPACITY, None, "introspection") + .await + .expect("failed to create cache storage") +} + /// A cache containing our well known introspection queries. pub(crate) struct Introspection { cache: CacheStorage, - planner: Arc>, + pub(crate) planner: Arc>, } impl Introspection { - pub(crate) async fn with_capacity( + pub(crate) async fn with_cache( planner: Arc>, - capacity: NonZeroUsize, + cache: CacheStorage, ) -> Result { - Ok(Self { - cache: CacheStorage::new(capacity, None, "introspection").await?, - planner, - }) - } - - pub(crate) async fn new(planner: Arc>) -> Result { - Self::with_capacity(planner, DEFAULT_INTROSPECTION_CACHE_CAPACITY).await + Ok(Self { cache, planner }) } #[cfg(test)] @@ -40,7 +40,11 @@ impl Introspection { planner: Arc>, cache: HashMap, ) -> Result { - let this = Self::with_capacity(planner, cache.len().try_into().unwrap()).await?; + let this = Self::with_cache( + planner, + CacheStorage::new(cache.len().try_into().unwrap(), None, "introspection").await?, + ) + .await?; for (query, response) in cache.into_iter() { this.cache.insert(query, response).await; diff --git a/apollo-router/src/json_ext.rs b/apollo-router/src/json_ext.rs index 7e26929f24..c9e617635f 100644 --- a/apollo-router/src/json_ext.rs +++ b/apollo-router/src/json_ext.rs @@ -5,6 +5,7 @@ use std::cmp::min; use std::fmt; +use num_traits::ToPrimitive; use once_cell::sync::Lazy; use regex::Captures; use regex::Regex; @@ -144,6 +145,11 @@ pub(crate) trait ValueExt { /// function to handle `PathElement::Fragment`). #[track_caller] fn is_object_of_type(&self, schema: &Schema, maybe_type: &str) -> bool; + + /// Convert this value to an instance of `apollo_compiler::ast::Value` + fn to_ast(&self) -> apollo_compiler::ast::Value; + + fn as_i32(&self) -> Option; } impl ValueExt for Value { @@ -468,6 +474,41 @@ impl ValueExt for Value { typename == maybe_type || schema.is_subtype(maybe_type, typename) }) } + + fn to_ast(&self) -> apollo_compiler::ast::Value { + match self { + Value::Null => apollo_compiler::ast::Value::Null, + Value::Bool(b) => apollo_compiler::ast::Value::Boolean(*b), + Value::Number(n) if n.is_f64() => { + apollo_compiler::ast::Value::Float(n.as_f64().expect("is float").into()) + } + Value::Number(n) => { + apollo_compiler::ast::Value::Int((n.as_i64().expect("is int") as i32).into()) + } + Value::String(s) => apollo_compiler::ast::Value::String(s.as_str().to_string()), + Value::Array(inner_vars) => apollo_compiler::ast::Value::List( + inner_vars + .iter() + .map(|v| apollo_compiler::Node::new(v.to_ast())) + .collect(), + ), + Value::Object(inner_vars) => apollo_compiler::ast::Value::Object( + inner_vars + .iter() + .map(|(k, v)| { + ( + apollo_compiler::Name::new(k.as_str()).expect("is valid name"), + apollo_compiler::Node::new(v.to_ast()), + ) + }) + .collect(), + ), + } + } + + fn as_i32(&self) -> Option { + self.as_i64()?.to_i32() + } } fn filter_type_conditions(value: Value, type_conditions: &Option) -> Value { @@ -1164,16 +1205,23 @@ mod tests { Schema::parse( r#" schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query } - directive @core(feature: String!) repeatable on SCHEMA + directive @join__graph(name: String!, url: String!) on ENUM_VALUE + directive @link( url: String as: String for: link__Purpose import: [link__Import]) repeatable on SCHEMA + scalar link__Import enum join__Graph { - FAKE @join__graph(name:"fake" url: "http://localhost:4001/fake") + FAKE @join__graph(name:"fake" url: "http://localhost:4001/fake") + } + + enum link__Purpose { + SECURITY + EXECUTION } type Query { diff --git a/apollo-router/src/metrics/aggregation.rs b/apollo-router/src/metrics/aggregation.rs index 53f36aee88..d136f82e5a 100644 --- a/apollo-router/src/metrics/aggregation.rs +++ b/apollo-router/src/metrics/aggregation.rs @@ -99,9 +99,6 @@ pub(crate) enum InstrumentWrapper { F64Histogram { _keep_alive: Arc>, }, - U64Gauge { - _keep_alive: Arc>, - }, } #[derive(Eq, PartialEq, Hash)] diff --git a/apollo-router/src/plugin/test/mock/subgraph.rs b/apollo-router/src/plugin/test/mock/subgraph.rs index 8ee5e5465c..46321b585b 100644 --- a/apollo-router/src/plugin/test/mock/subgraph.rs +++ b/apollo-router/src/plugin/test/mock/subgraph.rs @@ -6,6 +6,8 @@ use std::collections::HashMap; use std::sync::Arc; use std::task::Poll; +use apollo_compiler::ast::Definition; +use apollo_compiler::ast::Document; use futures::future; use http::HeaderMap; use http::HeaderName; @@ -38,7 +40,15 @@ pub struct MockSubgraph { impl MockSubgraph { pub fn new(mocks: MockResponses) -> Self { Self { - mocks: Arc::new(mocks), + mocks: Arc::new( + mocks + .into_iter() + .map(|(mut req, res)| { + normalize(&mut req); + (req, res) + }) + .collect(), + ), extensions: None, subscription_stream: None, map_request_fn: None, @@ -91,11 +101,10 @@ impl MockSubgraphBuilder { /// /// the arguments must deserialize to `crate::graphql::Request` and `crate::graphql::Response` pub fn with_json(mut self, request: serde_json::Value, response: serde_json::Value) -> Self { - self.mocks.insert( - serde_json::from_value(request).unwrap(), - serde_json::from_value(response).unwrap(), - ); - + let mut request = serde_json::from_value(request).unwrap(); + normalize(&mut request); + self.mocks + .insert(request, serde_json::from_value(response).unwrap()); self } @@ -123,6 +132,22 @@ impl MockSubgraphBuilder { } } +// Normalize queries so that spaces and operation names +// don't have an impact on the cache +fn normalize(request: &mut Request) { + if let Some(q) = &request.query { + let mut doc = Document::parse(q.clone(), "request").unwrap(); + + if let Some(Definition::OperationDefinition(ref mut op)) = doc.definitions.first_mut() { + let o = op.make_mut(); + o.name.take(); + }; + + request.query = Some(doc.serialize().no_indent().to_string()); + request.operation_name = None; + } +} + impl Service for MockSubgraph { type Response = SubgraphResponse; @@ -173,6 +198,8 @@ impl Service for MockSubgraph { } } + normalize(body); + let response = if let Some(response) = self.mocks.get(body) { // Build an http Response let mut http_response_builder = http::Response::builder().status(StatusCode::OK); diff --git a/apollo-router/src/plugins/authorization/authenticated.rs b/apollo-router/src/plugins/authorization/authenticated.rs index ffe881877b..15bcd7a969 100644 --- a/apollo-router/src/plugins/authorization/authenticated.rs +++ b/apollo-router/src/plugins/authorization/authenticated.rs @@ -13,6 +13,7 @@ use tower::BoxError; use crate::json_ext::Path; use crate::json_ext::PathElement; use crate::spec::query::transform; +use crate::spec::query::transform::TransformState; use crate::spec::query::traverse; use crate::spec::Schema; use crate::spec::TYPENAME; @@ -175,13 +176,13 @@ impl<'a> traverse::Visitor for AuthenticatedCheckVisitor<'a> { pub(crate) struct AuthenticatedVisitor<'a> { schema: &'a schema::Schema, - fragments: HashMap<&'a Name, &'a ast::FragmentDefinition>, + state: TransformState, implementers_map: &'a apollo_compiler::collections::HashMap, pub(crate) query_requires_authentication: bool, pub(crate) unauthorized_paths: Vec, // store the error paths from fragments so we can add them at // the point of application - fragments_unauthorized_paths: HashMap<&'a Name, Vec>, + fragments_unauthorized_paths: HashMap>, current_path: Path, authenticated_directive_name: String, dry_run: bool, @@ -190,13 +191,12 @@ pub(crate) struct AuthenticatedVisitor<'a> { impl<'a> AuthenticatedVisitor<'a> { pub(crate) fn new( schema: &'a schema::Schema, - executable: &'a ast::Document, implementers_map: &'a apollo_compiler::collections::HashMap, dry_run: bool, ) -> Option { Some(Self { schema, - fragments: transform::collect_fragments(executable), + state: TransformState::new(), implementers_map, dry_run, query_requires_authentication: false, @@ -409,17 +409,11 @@ impl<'a> transform::Visitor for AuthenticatedVisitor<'a> { }; if self.unauthorized_paths.len() > current_unauthorized_paths_index { - if let Some((name, _)) = self.fragments.get_key_value(&node.name) { - self.fragments_unauthorized_paths.insert( - name, - self.unauthorized_paths - .split_off(current_unauthorized_paths_index), - ); - } - } - - if let Ok(None) = res { - self.fragments.remove(&node.name); + self.fragments_unauthorized_paths.insert( + node.name.as_str().to_string(), + self.unauthorized_paths + .split_off(current_unauthorized_paths_index), + ); } res @@ -430,29 +424,35 @@ impl<'a> transform::Visitor for AuthenticatedVisitor<'a> { node: &ast::FragmentSpread, ) -> Result, BoxError> { // record the fragment errors at the point of application - if let Some(paths) = self.fragments_unauthorized_paths.get(&node.fragment_name) { + if let Some(paths) = self + .fragments_unauthorized_paths + .get(node.fragment_name.as_str()) + { for path in paths { let path = self.current_path.join(path); self.unauthorized_paths.push(path); } } - let fragment = match self.fragments.get(&node.fragment_name) { - Some(fragment) => fragment, + let condition = match self + .state() + .fragments() + .get(node.fragment_name.as_str()) + .map(|fragment| fragment.fragment.type_condition.clone()) + { + Some(condition) => condition, None => return Ok(None), }; - let condition = &fragment.type_condition; - - self.current_path - .push(PathElement::Fragment(condition.as_str().into())); - let fragment_requires_authentication = self .schema .types - .get(condition) + .get(condition.as_str()) .is_some_and(|type_definition| self.is_type_authenticated(type_definition)); + self.current_path + .push(PathElement::Fragment(condition.as_str().into())); + let res = if fragment_requires_authentication { self.query_requires_authentication = true; self.unauthorized_paths.push(self.current_path.clone()); @@ -515,6 +515,10 @@ impl<'a> transform::Visitor for AuthenticatedVisitor<'a> { fn schema(&self) -> &apollo_compiler::Schema { self.schema } + + fn state(&mut self) -> &mut TransformState { + &mut self.state + } } #[cfg(test)] @@ -609,7 +613,7 @@ mod tests { let doc = ast::Document::parse(query, "query.graphql").unwrap(); let map = schema.implementers_map(); - let mut visitor = AuthenticatedVisitor::new(&schema, &doc, &map, false).unwrap(); + let mut visitor = AuthenticatedVisitor::new(&schema, &map, false).unwrap(); ( transform::document(&mut visitor, &doc).unwrap(), @@ -1689,4 +1693,300 @@ mod tests { assert!(response.next_response().await.is_none()); } + + static AUTHENTICATED_ROOT_TYPE_SCHEMA: &str = r#" + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/authenticated/v0.1", for: SECURITY) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean + ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + directive @join__graph(name: String!, url: String!) on ENUM_VALUE + directive @join__implements( + graph: join__Graph! + interface: String! + ) repeatable on OBJECT | INTERFACE + directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false + ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + + scalar join__FieldSet + scalar link__Import + enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION + } + + enum join__Graph { + USER @join__graph(name: "user", url: "http://localhost:4001/graphql") + ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") + } + + type Query @join__type(graph: USER) @authenticated { + t: T @join__field(graph: USER) + } + + type T @join__type(graph: USER) { + f: String @join__field(graph: USER) + } + "#; + + #[test] + fn named_fragment_nested_in_authenticated_type() { + static QUERY: &str = r#" + query { + t { + ... F + } + } + + fragment F on T { + f + } + "#; + + let (doc, paths) = filter(AUTHENTICATED_ROOT_TYPE_SCHEMA, QUERY); + + insta::assert_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); + } + + static AUTHENTICATED_TYPE_SCHEMA: &str = r#" + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/authenticated/v0.1", for: SECURITY) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean + ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + directive @join__graph(name: String!, url: String!) on ENUM_VALUE + directive @join__implements( + graph: join__Graph! + interface: String! + ) repeatable on OBJECT | INTERFACE + directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false + ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + + scalar join__FieldSet + scalar link__Import + enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION + } + + enum join__Graph { + USER @join__graph(name: "user", url: "http://localhost:4001/graphql") + ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") + } + + type Query @join__type(graph: USER){ + t: T @join__field(graph: USER) + u(u:Int): String @join__field(graph: USER) + v(v:Int): Int @join__field(graph: USER) + } + + type T @join__type(graph: USER) @authenticated { + f(id: String): String @join__field(graph: USER) + } + "#; + + #[test] + fn named_fragment_nested_in_named_fragment_in_authenticated_type() { + static QUERY: &str = r#" + query A($v: Int) { + ... F3 + } + + query B($id:String, $u:Int, $include:Boolean, $skip:Boolean) { + ... F1 + u(u:$u) @include(if: $include) + } + + fragment F1 on Query { + ... F2 + } + + fragment F2 on Query { + t { + ... F3 @skip(if: $skip) + } + } + + fragment F3 on T { + f(id: $id) + } + + fragment F4 on Query { + ...F5 + } + + fragment F5 on Query { + v(v: $v) + } + "#; + + let (doc, paths) = filter(AUTHENTICATED_TYPE_SCHEMA, QUERY); + + insta::assert_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); + } + + #[tokio::test] + async fn introspection_fragment_with_authenticated_root_query() { + static QUERY: &str = r#" + query { + __schema { + types { + ... TypeDef + } + } + } + + fragment TypeDef on __Type { + name + } + "#; + + let service = TestHarness::builder() + .configuration_json(serde_json::json!({ + "supergraph": { + "introspection": true + }, + "include_subgraph_errors": { + "all": true + }, + "authorization": { + "directives": { + "enabled": true + } + }})) + .unwrap() + .schema(AUTHENTICATED_ROOT_TYPE_SCHEMA) + .build_supergraph() + .await + .unwrap(); + + let request = supergraph::Request::fake_builder() + .query(QUERY) + .build() + .unwrap(); + + let mut response = service.oneshot(request).await.unwrap(); + + let first_response = response.next_response().await.unwrap(); + + insta::assert_json_snapshot!(first_response); + + assert!(response.next_response().await.is_none()); + } + + #[tokio::test] + async fn introspection_mixed_with_authenticated_fields() { + // Note: in https://github.com/apollographql/router/pull/5952/ we moved introspection handling + // before authorization filtering in bridge_query_planner.rs, relying on the fact that queries + // mixing introspection and concrete fields are not supported, so introspection answers right + // away. If this ever changes, we should make sure that unauthorized fields are still properly + // filtered out + static QUERY: &str = r#" + query { + __schema { + types { + ... TypeDef + } + } + + t { + f + } + } + + fragment TypeDef on __Type { + name + } + "#; + + let service = TestHarness::builder() + .configuration_json(serde_json::json!({ + "supergraph": { + "introspection": true + }, + "include_subgraph_errors": { + "all": true + }, + "authorization": { + "directives": { + "enabled": true + } + }})) + .unwrap() + .schema(AUTHENTICATED_TYPE_SCHEMA) + .build_supergraph() + .await + .unwrap(); + + let request = supergraph::Request::fake_builder() + .query(QUERY) + .build() + .unwrap(); + + let mut response = service.oneshot(request).await.unwrap(); + + let first_response = response.next_response().await.unwrap(); + + insta::assert_json_snapshot!(first_response); + + assert!(response.next_response().await.is_none()); + } } diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index 8ded941c2b..331641a726 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -437,7 +437,6 @@ impl AuthorizationPlugin { ) -> Result)>, QueryPlannerError> { if let Some(mut visitor) = AuthenticatedVisitor::new( schema.supergraph_schema(), - doc, &schema.implementers_map, dry_run, ) { @@ -475,7 +474,6 @@ impl AuthorizationPlugin { ) -> Result)>, QueryPlannerError> { if let Some(mut visitor) = ScopeFilteringVisitor::new( schema.supergraph_schema(), - doc, &schema.implementers_map, scopes.iter().cloned().collect(), dry_run, @@ -510,7 +508,6 @@ impl AuthorizationPlugin { ) -> Result)>, QueryPlannerError> { if let Some(mut visitor) = PolicyFilteringVisitor::new( schema.supergraph_schema(), - doc, &schema.implementers_map, policies.iter().cloned().collect(), dry_run, diff --git a/apollo-router/src/plugins/authorization/policy.rs b/apollo-router/src/plugins/authorization/policy.rs index df692cf388..821546a01c 100644 --- a/apollo-router/src/plugins/authorization/policy.rs +++ b/apollo-router/src/plugins/authorization/policy.rs @@ -20,6 +20,7 @@ use tower::BoxError; use crate::json_ext::Path; use crate::json_ext::PathElement; use crate::spec::query::transform; +use crate::spec::query::transform::TransformState; use crate::spec::query::traverse; use crate::spec::Schema; use crate::spec::TYPENAME; @@ -187,7 +188,7 @@ impl<'a> traverse::Visitor for PolicyExtractionVisitor<'a> { pub(crate) struct PolicyFilteringVisitor<'a> { schema: &'a schema::Schema, - fragments: HashMap<&'a Name, &'a ast::FragmentDefinition>, + state: TransformState, implementers_map: &'a apollo_compiler::collections::HashMap, dry_run: bool, request_policies: HashSet, @@ -195,7 +196,7 @@ pub(crate) struct PolicyFilteringVisitor<'a> { pub(crate) unauthorized_paths: Vec, // store the error paths from fragments so we can add them at // the point of application - fragments_unauthorized_paths: HashMap<&'a Name, Vec>, + fragments_unauthorized_paths: HashMap>, current_path: Path, policy_directive_name: String, } @@ -222,14 +223,13 @@ fn policies_sets_argument( impl<'a> PolicyFilteringVisitor<'a> { pub(crate) fn new( schema: &'a schema::Schema, - executable: &'a ast::Document, implementers_map: &'a apollo_compiler::collections::HashMap, successful_policies: HashSet, dry_run: bool, ) -> Option { Some(Self { schema, - fragments: transform::collect_fragments(executable), + state: TransformState::new(), implementers_map, dry_run, request_policies: successful_policies, @@ -525,17 +525,11 @@ impl<'a> transform::Visitor for PolicyFilteringVisitor<'a> { }; if self.unauthorized_paths.len() > current_unauthorized_paths_index { - if let Some((name, _)) = self.fragments.get_key_value(&node.name) { - self.fragments_unauthorized_paths.insert( - name, - self.unauthorized_paths - .split_off(current_unauthorized_paths_index), - ); - } - } - - if let Ok(None) = res { - self.fragments.remove(&node.name); + self.fragments_unauthorized_paths.insert( + node.name.as_str().to_string(), + self.unauthorized_paths + .split_off(current_unauthorized_paths_index), + ); } res @@ -546,29 +540,35 @@ impl<'a> transform::Visitor for PolicyFilteringVisitor<'a> { node: &ast::FragmentSpread, ) -> Result, BoxError> { // record the fragment errors at the point of application - if let Some(paths) = self.fragments_unauthorized_paths.get(&node.fragment_name) { + if let Some(paths) = self + .fragments_unauthorized_paths + .get(node.fragment_name.as_str()) + { for path in paths { let path = self.current_path.join(path); self.unauthorized_paths.push(path); } } - let fragment = match self.fragments.get(&node.fragment_name) { - Some(fragment) => fragment, + let condition = match self + .state() + .fragments() + .get(node.fragment_name.as_str()) + .map(|fragment| fragment.fragment.type_condition.clone()) + { + Some(condition) => condition, None => return Ok(None), }; - let condition = &fragment.type_condition; - - self.current_path - .push(PathElement::Fragment(condition.as_str().into())); - let fragment_is_authorized = self .schema .types - .get(condition) + .get(condition.as_str()) .is_some_and(|ty| self.is_type_authorized(ty)); + self.current_path + .push(PathElement::Fragment(condition.as_str().into())); + let res = if !fragment_is_authorized { self.query_requires_policies = true; self.unauthorized_paths.push(self.current_path.clone()); @@ -631,6 +631,10 @@ impl<'a> transform::Visitor for PolicyFilteringVisitor<'a> { fn schema(&self) -> &apollo_compiler::Schema { self.schema } + + fn state(&mut self) -> &mut TransformState { + &mut self.state + } } #[cfg(test)] @@ -744,8 +748,7 @@ mod tests { let doc = ast::Document::parse(query, "query.graphql").unwrap(); doc.to_executable_validate(&schema).unwrap(); let map = schema.implementers_map(); - let mut visitor = - PolicyFilteringVisitor::new(&schema, &doc, &map, policies, false).unwrap(); + let mut visitor = PolicyFilteringVisitor::new(&schema, &map, policies, false).unwrap(); ( transform::document(&mut visitor, &doc).unwrap(), visitor.unauthorized_paths, diff --git a/apollo-router/src/plugins/authorization/scopes.rs b/apollo-router/src/plugins/authorization/scopes.rs index 361b50daad..55a4d0a0c4 100644 --- a/apollo-router/src/plugins/authorization/scopes.rs +++ b/apollo-router/src/plugins/authorization/scopes.rs @@ -20,6 +20,7 @@ use tower::BoxError; use crate::json_ext::Path; use crate::json_ext::PathElement; use crate::spec::query::transform; +use crate::spec::query::transform::TransformState; use crate::spec::query::traverse; use crate::spec::Schema; use crate::spec::TYPENAME; @@ -204,14 +205,14 @@ fn scopes_sets_argument(directive: &ast::Directive) -> impl Iterator { schema: &'a schema::Schema, - fragments: HashMap<&'a Name, &'a ast::FragmentDefinition>, + state: TransformState, implementers_map: &'a apollo_compiler::collections::HashMap, request_scopes: HashSet, pub(crate) query_requires_scopes: bool, pub(crate) unauthorized_paths: Vec, // store the error paths from fragments so we can add them at // the point of application - fragments_unauthorized_paths: HashMap<&'a Name, Vec>, + fragments_unauthorized_paths: HashMap>, current_path: Path, requires_scopes_directive_name: String, dry_run: bool, @@ -220,14 +221,13 @@ pub(crate) struct ScopeFilteringVisitor<'a> { impl<'a> ScopeFilteringVisitor<'a> { pub(crate) fn new( schema: &'a schema::Schema, - executable: &'a ast::Document, implementers_map: &'a apollo_compiler::collections::HashMap, scopes: HashSet, dry_run: bool, ) -> Option { Some(Self { schema, - fragments: transform::collect_fragments(executable), + state: TransformState::new(), implementers_map, request_scopes: scopes, dry_run, @@ -527,17 +527,11 @@ impl<'a> transform::Visitor for ScopeFilteringVisitor<'a> { }; if self.unauthorized_paths.len() > current_unauthorized_paths_index { - if let Some((name, _)) = self.fragments.get_key_value(&node.name) { - self.fragments_unauthorized_paths.insert( - name, - self.unauthorized_paths - .split_off(current_unauthorized_paths_index), - ); - } - } - - if let Ok(None) = res { - self.fragments.remove(&node.name); + self.fragments_unauthorized_paths.insert( + node.name.as_str().to_string(), + self.unauthorized_paths + .split_off(current_unauthorized_paths_index), + ); } res @@ -548,29 +542,35 @@ impl<'a> transform::Visitor for ScopeFilteringVisitor<'a> { node: &ast::FragmentSpread, ) -> Result, BoxError> { // record the fragment errors at the point of application - if let Some(paths) = self.fragments_unauthorized_paths.get(&node.fragment_name) { + if let Some(paths) = self + .fragments_unauthorized_paths + .get(node.fragment_name.as_str()) + { for path in paths { let path = self.current_path.join(path); self.unauthorized_paths.push(path); } } - let fragment = match self.fragments.get(&node.fragment_name) { - Some(fragment) => fragment, + let condition = match self + .state() + .fragments() + .get(node.fragment_name.as_str()) + .map(|fragment| fragment.fragment.type_condition.clone()) + { + Some(condition) => condition, None => return Ok(None), }; - let condition = &fragment.type_condition; - - self.current_path - .push(PathElement::Fragment(condition.as_str().into())); - let fragment_is_authorized = self .schema .types - .get(condition) + .get(condition.as_str()) .is_some_and(|ty| self.is_type_authorized(ty)); + self.current_path + .push(PathElement::Fragment(condition.as_str().into())); + let res = if !fragment_is_authorized { self.query_requires_scopes = true; self.unauthorized_paths.push(self.current_path.clone()); @@ -633,6 +633,10 @@ impl<'a> transform::Visitor for ScopeFilteringVisitor<'a> { fn schema(&self) -> &apollo_compiler::Schema { self.schema } + + fn state(&mut self) -> &mut TransformState { + &mut self.state + } } #[cfg(test)] @@ -748,7 +752,7 @@ mod tests { doc.to_executable_validate(&schema).unwrap(); let map = schema.implementers_map(); - let mut visitor = ScopeFilteringVisitor::new(&schema, &doc, &map, scopes, false).unwrap(); + let mut visitor = ScopeFilteringVisitor::new(&schema, &map, scopes, false).unwrap(); ( transform::document(&mut visitor, &doc).unwrap(), visitor.unauthorized_paths, diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__introspection_fragment_with_authenticated_root_query.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__introspection_fragment_with_authenticated_root_query.snap new file mode 100644 index 0000000000..b7f895cc65 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__introspection_fragment_with_authenticated_root_query.snap @@ -0,0 +1,48 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: first_response +--- +{ + "data": { + "__schema": { + "types": [ + { + "name": "Query" + }, + { + "name": "T" + }, + { + "name": "String" + }, + { + "name": "Boolean" + }, + { + "name": "__Schema" + }, + { + "name": "__Type" + }, + { + "name": "__TypeKind" + }, + { + "name": "__Field" + }, + { + "name": "__InputValue" + }, + { + "name": "__EnumValue" + }, + { + "name": "__Directive" + }, + { + "name": "__DirectiveLocation" + } + ] + } + } +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__introspection_mixed_with_authenticated_fields.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__introspection_mixed_with_authenticated_fields.snap new file mode 100644 index 0000000000..90938cf936 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__introspection_mixed_with_authenticated_fields.snap @@ -0,0 +1,14 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: first_response +--- +{ + "errors": [ + { + "message": "Mixed queries with both schema introspection and concrete fields are not supported", + "extensions": { + "code": "MIXED_INTROSPECTION" + } + } + ] +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__named_fragment_nested_in_authenticated_type.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__named_fragment_nested_in_authenticated_type.snap new file mode 100644 index 0000000000..8a6196b0d8 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__named_fragment_nested_in_authenticated_type.snap @@ -0,0 +1,19 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: "TestResult { query: QUERY, result: doc, paths }" +--- +query: + + query { + t { + ... F + } + } + + fragment F on T { + f + } + +filtered: + +paths: [""] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__named_fragment_nested_in_named_fragment_in_authenticated_type.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__named_fragment_nested_in_named_fragment_in_authenticated_type.snap new file mode 100644 index 0000000000..f2370303d1 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__named_fragment_nested_in_named_fragment_in_authenticated_type.snap @@ -0,0 +1,43 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: "TestResult { query: QUERY, result: doc, paths }" +--- +query: + + query A($v: Int) { + ... F3 + } + + query B($id:String, $u:Int, $include:Boolean, $skip:Boolean) { + ... F1 + u(u:$u) @include(if: $include) + } + + fragment F1 on Query { + ... F2 + } + + fragment F2 on Query { + t { + ... F3 @skip(if: $skip) + } + } + + fragment F3 on T { + f(id: $id) + } + + fragment F4 on Query { + ...F5 + } + + fragment F5 on Query { + v(v: $v) + } + +filtered: +query B($u: Int, $include: Boolean) { + u(u: $u) @include(if: $include) +} + +paths: ["", "/t"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-2.snap index 0ae4015930..fd2f876120 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-2.snap @@ -21,11 +21,7 @@ query: extracted_policies: {"read user", "read username"} successful policies: ["read user", "read username"] filtered: -fragment F on User { - name -} - -query { +{ topProducts { type } @@ -35,4 +31,8 @@ query { } } +fragment F on User { + name +} + paths: [] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap index 1028c92ffc..709b16a40b 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap @@ -22,11 +22,7 @@ query: extracted_scopes: {"read:user", "read:username"} request scopes: ["read:user"] filtered: -fragment F on User { - id2: id -} - -query { +{ topProducts { type } @@ -36,4 +32,8 @@ query { } } +fragment F on User { + id2: id +} + paths: ["/itf/name"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap index 9496930257..410f0562cb 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap @@ -22,12 +22,7 @@ query: extracted_scopes: {"read:user", "read:username"} request scopes: ["read:user", "read:username"] filtered: -fragment F on User { - id2: id - name -} - -query { +{ topProducts { type } @@ -37,4 +32,9 @@ query { } } +fragment F on User { + id2: id + name +} + paths: [] diff --git a/apollo-router/src/plugins/authorization/tests.rs b/apollo-router/src/plugins/authorization/tests.rs index 813533c184..d967125881 100644 --- a/apollo-router/src/plugins/authorization/tests.rs +++ b/apollo-router/src/plugins/authorization/tests.rs @@ -15,47 +15,7 @@ use crate::Context; use crate::MockedSubgraphs; use crate::TestHarness; -const SCHEMA: &str = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query -} -directive @core(feature: String!) repeatable on SCHEMA -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE -directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION -scalar join__FieldSet -enum join__Graph { - USER @join__graph(name: "user", url: "http://localhost:4001/graphql") - ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") -} -type Query { - currentUser: User @join__field(graph: USER) - orga(id: ID): Organization @join__field(graph: ORGA) -} -type User -@join__owner(graph: USER) -@join__type(graph: ORGA, key: "id") -@join__type(graph: USER, key: "id"){ - id: ID! - name: String - phone: String - activeOrganization: Organization -} -type Organization -@join__owner(graph: ORGA) -@join__type(graph: ORGA, key: "id") -@join__type(graph: USER, key: "id") { - id: ID - creatorUser: User - name: String - nonNullId: ID! - suborga: [Organization] -}"#; +const SCHEMA: &str = include_str!("../../testdata/orga_supergraph.graphql"); #[tokio::test] async fn authenticated_request() { diff --git a/apollo-router/src/plugins/cache/entity.rs b/apollo-router/src/plugins/cache/entity.rs index 3992dbd670..b0abd0178e 100644 --- a/apollo-router/src/plugins/cache/entity.rs +++ b/apollo-router/src/plugins/cache/entity.rs @@ -78,8 +78,8 @@ pub(crate) struct EntityCache { } pub(crate) struct Storage { - all: Option, - subgraphs: HashMap, + pub(crate) all: Option, + pub(crate) subgraphs: HashMap, } impl Storage { @@ -278,7 +278,20 @@ impl Plugin for EntityCache { subgraphs: subgraph_storages, }); - let invalidation = Invalidation::new(storage.clone()).await?; + let invalidation = Invalidation::new( + storage.clone(), + init.config + .invalidation + .as_ref() + .map(|i| i.scan_count) + .unwrap_or(1000), + init.config + .invalidation + .as_ref() + .map(|i| i.concurrent_requests) + .unwrap_or(10), + ) + .await?; Ok(Self { storage, @@ -448,7 +461,7 @@ impl EntityCache { all: Some(storage), subgraphs: HashMap::new(), }); - let invalidation = Invalidation::new(storage.clone()).await?; + let invalidation = Invalidation::new(storage.clone(), 1000, 10).await?; Ok(Self { storage, @@ -466,6 +479,8 @@ impl EntityCache { IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 4000, )), + scan_count: 1000, + concurrent_requests: 10, })), invalidation, }) diff --git a/apollo-router/src/plugins/cache/invalidation.rs b/apollo-router/src/plugins/cache/invalidation.rs index a566dc6f33..45d623299c 100644 --- a/apollo-router/src/plugins/cache/invalidation.rs +++ b/apollo-router/src/plugins/cache/invalidation.rs @@ -3,13 +3,14 @@ use std::time::Instant; use fred::error::RedisError; use fred::types::Scanner; +use futures::stream; use futures::StreamExt; use itertools::Itertools; use serde::Deserialize; use serde::Serialize; use serde_json_bytes::Value; use thiserror::Error; -use tokio::sync::broadcast; +use tokio::sync::Semaphore; use tower::BoxError; use tracing::Instrument; @@ -19,16 +20,11 @@ use crate::cache::redis::RedisKey; use crate::plugins::cache::entity::hash_entity_key; use crate::plugins::cache::entity::ENTITY_CACHE_VERSION; -const CHANNEL_SIZE: usize = 1024; - #[derive(Clone)] pub(crate) struct Invalidation { - #[allow(clippy::type_complexity)] - pub(super) handle: tokio::sync::mpsc::Sender<( - Vec, - InvalidationOrigin, - broadcast::Sender>, - )>, + pub(crate) storage: Arc, + pub(crate) scan_count: u32, + pub(crate) semaphore: Arc, } #[derive(Error, Debug, Clone)] @@ -37,9 +33,6 @@ pub(crate) enum InvalidationError { RedisError(#[from] RedisError), #[error("several errors")] Errors(#[from] InvalidationErrors), - #[cfg(test)] - #[error("custom error: {0}")] - Custom(String), } #[derive(Debug, Clone)] @@ -67,52 +60,23 @@ pub(crate) enum InvalidationOrigin { } impl Invalidation { - pub(crate) async fn new(storage: Arc) -> Result { - let (tx, rx) = tokio::sync::mpsc::channel(CHANNEL_SIZE); - - tokio::task::spawn(async move { - start(storage, rx).await; - }); - Ok(Self { handle: tx }) + pub(crate) async fn new( + storage: Arc, + scan_count: u32, + concurrent_requests: u32, + ) -> Result { + Ok(Self { + storage, + scan_count, + semaphore: Arc::new(Semaphore::new(concurrent_requests as usize)), + }) } pub(crate) async fn invalidate( - &mut self, + &self, origin: InvalidationOrigin, requests: Vec, ) -> Result { - let (response_tx, mut response_rx) = broadcast::channel(2); - self.handle - .send((requests, origin, response_tx.clone())) - .await - .map_err(|e| format!("cannot send invalidation request: {e}"))?; - - let result = response_rx - .recv() - .await - .map_err(|err| { - format!( - "cannot receive response for invalidation request: {:?}", - err - ) - })? - .map_err(|err| format!("received an invalidation error: {:?}", err))?; - - Ok(result) - } -} - -// TODO refactor -#[allow(clippy::type_complexity)] -async fn start( - storage: Arc, - mut handle: tokio::sync::mpsc::Receiver<( - Vec, - InvalidationOrigin, - broadcast::Sender>, - )>, -) { - while let Some((requests, origin, response_tx)) = handle.recv().await { let origin = match origin { InvalidationOrigin::Endpoint => "endpoint", InvalidationOrigin::Extensions => "extensions", @@ -124,117 +88,130 @@ async fn start( "origin" = origin ); - if let Err(err) = response_tx.send( - handle_request_batch(&storage, origin, requests) - .instrument(tracing::info_span!( - "cache.invalidation.batch", - "origin" = origin - )) - .await, - ) { - ::tracing::error!("cannot send answer to invalidation request in the channel: {err}"); - } + Ok(self + .handle_request_batch(origin, requests) + .instrument(tracing::info_span!( + "cache.invalidation.batch", + "origin" = origin + )) + .await?) } -} -async fn handle_request( - storage: &RedisCacheStorage, - origin: &'static str, - request: &InvalidationRequest, -) -> Result { - let key_prefix = request.key_prefix(); - let subgraph = request.subgraph_name(); - tracing::debug!( - "got invalidation request: {request:?}, will scan for: {}", - key_prefix - ); - - // FIXME: configurable batch size - let mut stream = storage.scan(key_prefix.clone(), Some(100)); - let mut count = 0u64; - let mut error = None; + async fn handle_request( + &self, + redis_storage: &RedisCacheStorage, + origin: &'static str, + request: &InvalidationRequest, + ) -> Result { + let key_prefix = request.key_prefix(); + let subgraph = request.subgraph_name(); + tracing::debug!( + "got invalidation request: {request:?}, will scan for: {}", + key_prefix + ); - while let Some(res) = stream.next().await { - match res { - Err(e) => { - tracing::error!( - pattern = key_prefix, - error = %e, - message = "error scanning for key", - ); - error = Some(e); - break; - } - Ok(scan_res) => { - if let Some(keys) = scan_res.results() { - let keys = keys - .iter() - .filter_map(|k| k.as_str()) - .map(|k| RedisKey(k.to_string())) - .collect::>(); - if !keys.is_empty() { - count += keys.len() as u64; - storage.delete(keys).await; + let mut stream = redis_storage.scan(key_prefix.clone(), Some(self.scan_count)); + let mut count = 0u64; + let mut error = None; - u64_counter!( - "apollo.router.operations.entity.invalidation.entry", - "Entity cache counter for invalidated entries", - 1u64, - "origin" = origin, - "subgraph.name" = subgraph.clone() - ); + while let Some(res) = stream.next().await { + match res { + Err(e) => { + tracing::error!( + pattern = key_prefix, + error = %e, + message = "error scanning for key", + ); + error = Some(e); + break; + } + Ok(scan_res) => { + if let Some(keys) = scan_res.results() { + let keys = keys + .iter() + .filter_map(|k| k.as_str()) + .map(|k| RedisKey(k.to_string())) + .collect::>(); + if !keys.is_empty() { + let deleted = redis_storage.delete(keys).await.unwrap_or(0) as u64; + count += deleted; + } } + scan_res.next()?; } - scan_res.next()?; } } - } - u64_histogram!( - "apollo.router.cache.invalidation.keys", - "Number of invalidated keys.", - count - ); + u64_counter!( + "apollo.router.operations.entity.invalidation.entry", + "Entity cache counter for invalidated entries", + count, + "origin" = origin, + "subgraph.name" = subgraph.clone() + ); - match error { - Some(err) => Err(err.into()), - None => Ok(count), + u64_histogram!( + "apollo.router.cache.invalidation.keys", + "Number of invalidated keys per invalidation request.", + count + ); + + match error { + Some(err) => Err(err.into()), + None => Ok(count), + } } -} -async fn handle_request_batch( - storage: &EntityStorage, - origin: &'static str, - requests: Vec, -) -> Result { - let mut count = 0; - let mut errors = Vec::new(); - for request in requests { - let start = Instant::now(); - let redis_storage = match storage.get(request.subgraph_name()) { - Some(s) => s, - None => continue, - }; - match handle_request(redis_storage, origin, &request) - .instrument(tracing::info_span!("cache.invalidation.request")) - .await - { - Ok(c) => count += c, - Err(err) => { - errors.push(err); + async fn handle_request_batch( + &self, + origin: &'static str, + requests: Vec, + ) -> Result { + let mut count = 0; + let mut errors = Vec::new(); + let mut futures = Vec::new(); + for request in requests { + let redis_storage = match self.storage.get(request.subgraph_name()) { + Some(s) => s, + None => continue, + }; + + let semaphore = self.semaphore.clone(); + let f = async move { + // limit the number of invalidation requests executing at any point in time + let _ = semaphore.acquire().await; + + let start = Instant::now(); + + let res = self + .handle_request(redis_storage, origin, &request) + .instrument(tracing::info_span!("cache.invalidation.request")) + .await; + + f64_histogram!( + "apollo.router.cache.invalidation.duration", + "Duration of the invalidation event execution.", + start.elapsed().as_secs_f64() + ); + res + }; + futures.push(f); + } + let mut stream: stream::FuturesUnordered<_> = futures.into_iter().collect(); + while let Some(res) = stream.next().await { + match res { + Ok(c) => count += c, + Err(err) => { + errors.push(err); + } } } - f64_histogram!( - "apollo.router.cache.invalidation.duration", - "Duration of the invalidation event execution.", - start.elapsed().as_secs_f64() - ); - } - if !errors.is_empty() { - Err(InvalidationErrors(errors).into()) - } else { - Ok(count) + if !errors.is_empty() { + Err(InvalidationErrors(errors).into()) + } else { + Ok(count) + } } } diff --git a/apollo-router/src/plugins/cache/invalidation_endpoint.rs b/apollo-router/src/plugins/cache/invalidation_endpoint.rs index 18af7763f1..0b1af7a9b3 100644 --- a/apollo-router/src/plugins/cache/invalidation_endpoint.rs +++ b/apollo-router/src/plugins/cache/invalidation_endpoint.rs @@ -39,6 +39,20 @@ pub(crate) struct InvalidationEndpointConfig { pub(crate) path: String, /// Listen address on which the invalidation endpoint must listen. pub(crate) listen: ListenAddr, + #[serde(default = "default_scan_count")] + /// Number of keys to return at once from a redis SCAN command + pub(crate) scan_count: u32, + #[serde(default = "concurrent_requests_count")] + /// Number of concurrent invalidation requests + pub(crate) concurrent_requests: u32, +} + +fn default_scan_count() -> u32 { + 1000 +} + +fn concurrent_requests_count() -> u32 { + 10 } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] @@ -82,7 +96,7 @@ impl Service for InvalidationService { } fn call(&mut self, req: router::Request) -> Self::Future { - let mut invalidation = self.invalidation.clone(); + let invalidation = self.invalidation.clone(); let config = self.config.clone(); Box::pin( async move { @@ -203,16 +217,23 @@ fn valid_shared_key( mod tests { use std::collections::HashMap; - use tokio::sync::broadcast; use tower::ServiceExt; use super::*; - use crate::plugins::cache::invalidation::InvalidationError; + use crate::cache::redis::RedisCacheStorage; + use crate::plugins::cache::entity::Storage; + use crate::plugins::cache::tests::MockStore; #[tokio::test] async fn test_invalidation_service_bad_shared_key() { - let (handle, _rx) = tokio::sync::mpsc::channel(128); - let invalidation = Invalidation { handle }; + let redis_cache = RedisCacheStorage::from_mocks(Arc::new(MockStore::new())) + .await + .unwrap(); + let storage = Arc::new(Storage { + all: Some(redis_cache), + subgraphs: HashMap::new(), + }); + let invalidation = Invalidation::new(storage.clone(), 1000, 10).await.unwrap(); let config = Arc::new(SubgraphConfiguration { all: Subgraph { @@ -250,108 +271,16 @@ mod tests { } #[tokio::test] - async fn test_invalidation_service_good_sub_shared_key() { - let (handle, mut rx) = tokio::sync::mpsc::channel::<( - Vec, - InvalidationOrigin, - broadcast::Sender>, - )>(128); - tokio::task::spawn(async move { - let mut called = false; - while let Some((requests, origin, response_tx)) = rx.recv().await { - called = true; - if requests - != [ - InvalidationRequest::Subgraph { - subgraph: String::from("test"), - }, - InvalidationRequest::Type { - subgraph: String::from("test"), - r#type: String::from("Test"), - }, - ] - { - response_tx - .send(Err(InvalidationError::Custom(format!( - "it's not the right invalidation requests : {requests:?}" - )))) - .unwrap(); - return; - } - if origin != InvalidationOrigin::Endpoint { - response_tx - .send(Err(InvalidationError::Custom(format!( - "it's not the right invalidation origin : {origin:?}" - )))) - .unwrap(); - return; - } - response_tx.send(Ok(0)).unwrap(); - } - assert!(called); - }); - - let invalidation = Invalidation { - handle: handle.clone(), - }; - let config = Arc::new(SubgraphConfiguration { - all: Subgraph { - ttl: None, - enabled: true, - redis: None, - private_id: None, - invalidation: Some(SubgraphInvalidationConfig { - enabled: true, - shared_key: String::from("test"), - }), - }, - subgraphs: [( - String::from("test"), - Subgraph { - ttl: None, - redis: None, - enabled: true, - private_id: None, - invalidation: Some(SubgraphInvalidationConfig { - enabled: true, - shared_key: String::from("test_test"), - }), - }, - )] - .into_iter() - .collect(), - }); - let service = InvalidationService::new(config, invalidation); - let req = router::Request::fake_builder() - .method(http::Method::POST) - .header(AUTHORIZATION, "test_test") - .body( - serde_json::to_vec(&[ - InvalidationRequest::Subgraph { - subgraph: String::from("test"), - }, - InvalidationRequest::Type { - subgraph: String::from("test"), - r#type: String::from("Test"), - }, - ]) - .unwrap(), - ) - .build() + async fn test_invalidation_service_bad_shared_key_subgraph() { + let redis_cache = RedisCacheStorage::from_mocks(Arc::new(MockStore::new())) + .await .unwrap(); - let res = service.oneshot(req).await.unwrap(); - assert_eq!(res.response.status(), StatusCode::ACCEPTED); - } + let storage = Arc::new(Storage { + all: Some(redis_cache), + subgraphs: HashMap::new(), + }); + let invalidation = Invalidation::new(storage.clone(), 1000, 10).await.unwrap(); - #[tokio::test] - async fn test_invalidation_service_bad_shared_key_subgraph() { - #[allow(clippy::type_complexity)] - let (handle, _rx) = tokio::sync::mpsc::channel::<( - Vec, - InvalidationOrigin, - broadcast::Sender>, - )>(128); - let invalidation = Invalidation { handle }; let config = Arc::new(SubgraphConfiguration { all: Subgraph { ttl: None, @@ -395,92 +324,4 @@ mod tests { let res = service.oneshot(req).await.unwrap(); assert_eq!(res.response.status(), StatusCode::UNAUTHORIZED); } - - #[tokio::test] - async fn test_invalidation_service() { - let (handle, mut rx) = tokio::sync::mpsc::channel::<( - Vec, - InvalidationOrigin, - broadcast::Sender>, - )>(128); - let invalidation = Invalidation { handle }; - - tokio::task::spawn(async move { - let mut called = false; - while let Some((requests, origin, response_tx)) = rx.recv().await { - called = true; - if requests - != [ - InvalidationRequest::Subgraph { - subgraph: String::from("test"), - }, - InvalidationRequest::Type { - subgraph: String::from("test"), - r#type: String::from("Test"), - }, - ] - { - response_tx - .send(Err(InvalidationError::Custom(format!( - "it's not the right invalidation requests : {requests:?}" - )))) - .unwrap(); - return; - } - if origin != InvalidationOrigin::Endpoint { - response_tx - .send(Err(InvalidationError::Custom(format!( - "it's not the right invalidation origin : {origin:?}" - )))) - .unwrap(); - return; - } - response_tx.send(Ok(2)).unwrap(); - } - assert!(called); - }); - - let config = Arc::new(SubgraphConfiguration { - all: Subgraph { - ttl: None, - enabled: true, - private_id: None, - redis: None, - invalidation: Some(SubgraphInvalidationConfig { - enabled: true, - shared_key: String::from("test"), - }), - }, - subgraphs: HashMap::new(), - }); - let service = InvalidationService::new(config, invalidation); - let req = router::Request::fake_builder() - .method(http::Method::POST) - .header(AUTHORIZATION, "test") - .body( - serde_json::to_vec(&[ - InvalidationRequest::Subgraph { - subgraph: String::from("test"), - }, - InvalidationRequest::Type { - subgraph: String::from("test"), - r#type: String::from("Test"), - }, - ]) - .unwrap(), - ) - .build() - .unwrap(); - let res = service.oneshot(req).await.unwrap(); - assert_eq!(res.response.status(), StatusCode::ACCEPTED); - assert_eq!( - serde_json::from_slice::( - &hyper::body::to_bytes(res.response.into_body()) - .await - .unwrap() - ) - .unwrap(), - serde_json::json!({"count": 2}) - ); - } } diff --git a/apollo-router/src/plugins/cache/tests.rs b/apollo-router/src/plugins/cache/tests.rs index 36628acf5e..64c96a0763 100644 --- a/apollo-router/src/plugins/cache/tests.rs +++ b/apollo-router/src/plugins/cache/tests.rs @@ -23,60 +23,14 @@ use crate::Context; use crate::MockedSubgraphs; use crate::TestHarness; -const SCHEMA: &str = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query - subscription: Subscription - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - scalar join__FieldSet - enum join__Graph { - USER @join__graph(name: "user", url: "http://localhost:4001/graphql") - ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") - } - type Query { - currentUser: User @join__field(graph: USER) - } - - type Subscription @join__type(graph: USER) { - userWasCreated: User - } - - type User - @join__owner(graph: USER) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id"){ - id: ID! - name: String - activeOrganization: Organization - allOrganizations: [Organization] - } - type Organization - @join__owner(graph: ORGA) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id") { - id: ID - creatorUser: User - name: String - nonNullId: ID! - suborga: [Organization] - }"#; - +const SCHEMA: &str = include_str!("../../testdata/orga_supergraph.graphql"); #[derive(Debug)] pub(crate) struct MockStore { map: Arc>>, } impl MockStore { - fn new() -> MockStore { + pub(crate) fn new() -> MockStore { MockStore { map: Arc::new(Mutex::new(HashMap::new())), } diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs b/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs index c4dcc36b00..6f23fcdc00 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/directives.rs @@ -17,7 +17,9 @@ use apollo_federation::link::spec::APOLLO_SPEC_DOMAIN; use apollo_federation::link::Link; use tower::BoxError; -use super::DemandControlError; +use crate::json_ext::Object; +use crate::json_ext::ValueExt; +use crate::plugins::demand_control::DemandControlError; const COST_DIRECTIVE_NAME: Name = name!("cost"); const COST_DIRECTIVE_DEFAULT_NAME: Name = name!("federation__cost"); @@ -203,9 +205,10 @@ impl DefinitionListSizeDirective { } } - pub(in crate::plugins::demand_control) fn with_field( + pub(in crate::plugins::demand_control) fn with_field_and_variables( &self, field: &Field, + variables: &Object, ) -> Result { let mut slicing_arguments: HashMap<&str, i32> = HashMap::new(); if let Some(slicing_argument_names) = self.slicing_argument_names.as_ref() { @@ -224,6 +227,13 @@ impl DefinitionListSizeDirective { if slicing_argument_names.contains(argument.name.as_str()) { if let Some(numeric_value) = argument.value.to_i32() { slicing_arguments.insert(&argument.name, numeric_value); + } else if let Some(numeric_value) = argument + .value + .as_variable() + .and_then(|variable_name| variables.get(variable_name.as_str())) + .and_then(|variable| variable.as_i32()) + { + slicing_arguments.insert(&argument.name, numeric_value); } } } diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_query_with_variable_slicing_argument.graphql b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_query_with_variable_slicing_argument.graphql new file mode 100644 index 0000000000..c3629cb0e0 --- /dev/null +++ b/apollo-router/src/plugins/demand_control/cost_calculator/fixtures/custom_cost_query_with_variable_slicing_argument.graphql @@ -0,0 +1,20 @@ +fragment Items on SizedField { + items { + id + } +} + +query VariableTestQuery($costlyInput: InputTypeWithCost, $fieldCountVar: Int) { + fieldWithCost + argWithCost(arg: 3) + enumWithCost + inputWithCost(someInput: $costlyInput) + scalarWithCost + objectWithCost { + id + } + fieldWithListSize + fieldWithDynamicListSize(first: $fieldCountVar) { + ...Items + } +} diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs index 439d09558f..752479b256 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs @@ -21,6 +21,8 @@ use super::schema::DemandControlledSchema; use super::DemandControlError; use crate::graphql::Response; use crate::graphql::ResponseVisitor; +use crate::json_ext::Object; +use crate::json_ext::ValueExt; use crate::plugins::demand_control::cost_calculator::directives::CostDirective; use crate::plugins::demand_control::cost_calculator::directives::ListSizeDirective; use crate::query_planner::fetch::SubgraphOperation; @@ -35,10 +37,18 @@ pub(crate) struct StaticCostCalculator { subgraph_schemas: Arc>, } +struct ScoringContext<'a> { + schema: &'a DemandControlledSchema, + query: &'a ExecutableDocument, + variables: &'a Object, + should_estimate_requires: bool, +} + fn score_argument( argument: &apollo_compiler::ast::Value, argument_definition: &Node, schema: &DemandControlledSchema, + variables: &Object, ) -> Result { let cost_directive = CostDirective::from_argument(schema.directive_name_map(), argument_definition); @@ -74,17 +84,26 @@ fn score_argument( argument_definition.ty.inner_named_type() )) })?; - cost += score_argument(arg_val, arg_def, schema)?; + cost += score_argument(arg_val, arg_def, schema, variables,)?; } Ok(cost) } (ast::Value::List(inner_args), _) => { let mut cost = cost_directive.map_or(0.0, |cost| cost.weight()); for arg_val in inner_args { - cost += score_argument(arg_val, argument_definition, schema)?; + cost += score_argument(arg_val, argument_definition, schema, variables)?; } Ok(cost) } + (ast::Value::Variable(name), _) => { + // We make a best effort attempt to score the variable, but some of these may not exist in the variables + // sent on the supergraph request, such as `$representations`. + if let Some(variable) = variables.get(name.as_str()) { + score_argument(&variable.to_ast(), argument_definition, schema, variables) + } else { + Ok(0.0) + } + } (ast::Value::Null, _) => Ok(0.0), _ => Ok(cost_directive.map_or(0.0, |cost| cost.weight())) } @@ -123,11 +142,9 @@ impl StaticCostCalculator { /// bound for cost anyway. fn score_field( &self, + ctx: &ScoringContext, field: &Field, parent_type: &NamedType, - schema: &DemandControlledSchema, - executable: &ExecutableDocument, - should_estimate_requires: bool, list_size_from_upstream: Option, ) -> Result { if StaticCostCalculator::skipped_by_directives(field) { @@ -136,19 +153,21 @@ impl StaticCostCalculator { // We need to look up the `FieldDefinition` from the supergraph schema instead of using `field.definition` // because `field.definition` was generated from the API schema, which strips off the directives we need. - let definition = schema.type_field(parent_type, &field.name)?; - let ty = field.inner_type_def(schema).ok_or_else(|| { + let definition = ctx.schema.type_field(parent_type, &field.name)?; + let ty = field.inner_type_def(ctx.schema).ok_or_else(|| { DemandControlError::QueryParseFailure(format!( "Field {} was found in query, but its type is missing from the schema.", field.name )) })?; - let list_size_directive = - match schema.type_field_list_size_directive(parent_type, &field.name) { - Some(dir) => dir.with_field(field).map(Some), - None => Ok(None), - }?; + let list_size_directive = match ctx + .schema + .type_field_list_size_directive(parent_type, &field.name) + { + Some(dir) => dir.with_field_and_variables(field, ctx.variables).map(Some), + None => Ok(None), + }?; let instance_count = if !field.ty().is_list() { 1 } else if let Some(value) = list_size_from_upstream { @@ -165,8 +184,9 @@ impl StaticCostCalculator { // Determine the cost for this particular field. Scalars are free, non-scalars are not. // For fields with selections, add in the cost of the selections as well. - let mut type_cost = if let Some(cost_directive) = - schema.type_field_cost_directive(parent_type, &field.name) + let mut type_cost = if let Some(cost_directive) = ctx + .schema + .type_field_cost_directive(parent_type, &field.name) { cost_directive.weight() } else if ty.is_interface() || ty.is_object() || ty.is_union() { @@ -175,11 +195,9 @@ impl StaticCostCalculator { 0.0 }; type_cost += self.score_selection_set( + ctx, &field.selection_set, field.ty().inner_named_type(), - schema, - executable, - should_estimate_requires, list_size_directive.as_ref(), )?; @@ -192,24 +210,28 @@ impl StaticCostCalculator { argument.name, field.name )) })?; - arguments_cost += score_argument(&argument.value, argument_definition, schema)?; + arguments_cost += score_argument( + &argument.value, + argument_definition, + ctx.schema, + ctx.variables, + )?; } let mut requirements_cost = 0.0; - if should_estimate_requires { + if ctx.should_estimate_requires { // If the field is marked with `@requires`, the required selection may not be included // in the query's selection. Adding that requirement's cost to the field ensures it's // accounted for. - let requirements = schema + let requirements = ctx + .schema .type_field_requires_directive(parent_type, &field.name) .map(|d| &d.fields); if let Some(selection_set) = requirements { requirements_cost = self.score_selection_set( + ctx, selection_set, parent_type, - schema, - executable, - should_estimate_requires, list_size_directive.as_ref(), )?; } @@ -231,44 +253,36 @@ impl StaticCostCalculator { fn score_fragment_spread( &self, + ctx: &ScoringContext, fragment_spread: &FragmentSpread, parent_type: &NamedType, - schema: &DemandControlledSchema, - executable: &ExecutableDocument, - should_estimate_requires: bool, list_size_directive: Option<&ListSizeDirective>, ) -> Result { - let fragment = fragment_spread.fragment_def(executable).ok_or_else(|| { + let fragment = fragment_spread.fragment_def(ctx.query).ok_or_else(|| { DemandControlError::QueryParseFailure(format!( "Parsed operation did not have a definition for fragment {}", fragment_spread.fragment_name )) })?; self.score_selection_set( + ctx, &fragment.selection_set, parent_type, - schema, - executable, - should_estimate_requires, list_size_directive, ) } fn score_inline_fragment( &self, + ctx: &ScoringContext, inline_fragment: &InlineFragment, parent_type: &NamedType, - schema: &DemandControlledSchema, - executable: &ExecutableDocument, - should_estimate_requires: bool, list_size_directive: Option<&ListSizeDirective>, ) -> Result { self.score_selection_set( + ctx, &inline_fragment.selection_set, parent_type, - schema, - executable, - should_estimate_requires, list_size_directive, ) } @@ -276,63 +290,43 @@ impl StaticCostCalculator { fn score_operation( &self, operation: &Operation, - schema: &DemandControlledSchema, - executable: &ExecutableDocument, - should_estimate_requires: bool, + ctx: &ScoringContext, ) -> Result { let mut cost = if operation.is_mutation() { 10.0 } else { 0.0 }; - let Some(root_type_name) = schema.root_operation(operation.operation_type) else { + let Some(root_type_name) = ctx.schema.root_operation(operation.operation_type) else { return Err(DemandControlError::QueryParseFailure(format!( "Cannot cost {} operation because the schema does not support this root type", operation.operation_type ))); }; - cost += self.score_selection_set( - &operation.selection_set, - root_type_name, - schema, - executable, - should_estimate_requires, - None, - )?; + cost += self.score_selection_set(ctx, &operation.selection_set, root_type_name, None)?; Ok(cost) } fn score_selection( &self, + ctx: &ScoringContext, selection: &Selection, parent_type: &NamedType, - schema: &DemandControlledSchema, - executable: &ExecutableDocument, - should_estimate_requires: bool, list_size_directive: Option<&ListSizeDirective>, ) -> Result { match selection { Selection::Field(f) => self.score_field( + ctx, f, parent_type, - schema, - executable, - should_estimate_requires, list_size_directive.and_then(|dir| dir.size_of(f)), ), - Selection::FragmentSpread(s) => self.score_fragment_spread( - s, - parent_type, - schema, - executable, - should_estimate_requires, - list_size_directive, - ), + Selection::FragmentSpread(s) => { + self.score_fragment_spread(ctx, s, parent_type, list_size_directive) + } Selection::InlineFragment(i) => self.score_inline_fragment( + ctx, i, i.type_condition.as_ref().unwrap_or(parent_type), - schema, - executable, - should_estimate_requires, list_size_directive, ), } @@ -340,23 +334,14 @@ impl StaticCostCalculator { fn score_selection_set( &self, + ctx: &ScoringContext, selection_set: &SelectionSet, parent_type_name: &NamedType, - schema: &DemandControlledSchema, - executable: &ExecutableDocument, - should_estimate_requires: bool, list_size_directive: Option<&ListSizeDirective>, ) -> Result { let mut cost = 0.0; for selection in selection_set.selections.iter() { - cost += self.score_selection( - selection, - parent_type_name, - schema, - executable, - should_estimate_requires, - list_size_directive, - )?; + cost += self.score_selection(ctx, selection, parent_type_name, list_size_directive)?; } Ok(cost) } @@ -375,25 +360,33 @@ impl StaticCostCalculator { false } - fn score_plan_node(&self, plan_node: &PlanNode) -> Result { + fn score_plan_node( + &self, + plan_node: &PlanNode, + variables: &Object, + ) -> Result { match plan_node { - PlanNode::Sequence { nodes } => self.summed_score_of_nodes(nodes), - PlanNode::Parallel { nodes } => self.summed_score_of_nodes(nodes), - PlanNode::Flatten(flatten_node) => self.score_plan_node(&flatten_node.node), + PlanNode::Sequence { nodes } => self.summed_score_of_nodes(nodes, variables), + PlanNode::Parallel { nodes } => self.summed_score_of_nodes(nodes, variables), + PlanNode::Flatten(flatten_node) => self.score_plan_node(&flatten_node.node, variables), PlanNode::Condition { condition: _, if_clause, else_clause, - } => self.max_score_of_nodes(if_clause, else_clause), + } => self.max_score_of_nodes(if_clause, else_clause, variables), PlanNode::Defer { primary, deferred } => { - self.summed_score_of_deferred_nodes(primary, deferred) - } - PlanNode::Fetch(fetch_node) => { - self.estimated_cost_of_operation(&fetch_node.service_name, &fetch_node.operation) - } - PlanNode::Subscription { primary, rest: _ } => { - self.estimated_cost_of_operation(&primary.service_name, &primary.operation) + self.summed_score_of_deferred_nodes(primary, deferred, variables) } + PlanNode::Fetch(fetch_node) => self.estimated_cost_of_operation( + &fetch_node.service_name, + &fetch_node.operation, + variables, + ), + PlanNode::Subscription { primary, rest: _ } => self.estimated_cost_of_operation( + &primary.service_name, + &primary.operation, + variables, + ), } } @@ -401,6 +394,7 @@ impl StaticCostCalculator { &self, subgraph: &str, operation: &SubgraphOperation, + variables: &Object, ) -> Result { tracing::debug!("On subgraph {}, scoring operation: {}", subgraph, operation); @@ -414,21 +408,22 @@ impl StaticCostCalculator { let operation = operation .as_parsed() .map_err(DemandControlError::SubgraphOperationNotInitialized)?; - self.estimated(operation, schema, false) + self.estimated(operation, schema, variables, false) } fn max_score_of_nodes( &self, left: &Option>, right: &Option>, + variables: &Object, ) -> Result { match (left, right) { (None, None) => Ok(0.0), - (None, Some(right)) => self.score_plan_node(right), - (Some(left), None) => self.score_plan_node(left), + (None, Some(right)) => self.score_plan_node(right, variables), + (Some(left), None) => self.score_plan_node(left, variables), (Some(left), Some(right)) => { - let left_score = self.score_plan_node(left)?; - let right_score = self.score_plan_node(right)?; + let left_score = self.score_plan_node(left, variables)?; + let right_score = self.score_plan_node(right, variables)?; Ok(left_score.max(right_score)) } } @@ -438,23 +433,28 @@ impl StaticCostCalculator { &self, primary: &Primary, deferred: &Vec, + variables: &Object, ) -> Result { let mut score = 0.0; if let Some(node) = &primary.node { - score += self.score_plan_node(node)?; + score += self.score_plan_node(node, variables)?; } for d in deferred { if let Some(node) = &d.node { - score += self.score_plan_node(node)?; + score += self.score_plan_node(node, variables)?; } } Ok(score) } - fn summed_score_of_nodes(&self, nodes: &Vec) -> Result { + fn summed_score_of_nodes( + &self, + nodes: &Vec, + variables: &Object, + ) -> Result { let mut sum = 0.0; for node in nodes { - sum += self.score_plan_node(node)?; + sum += self.score_plan_node(node, variables)?; } Ok(sum) } @@ -463,29 +463,41 @@ impl StaticCostCalculator { &self, query: &ExecutableDocument, schema: &DemandControlledSchema, + variables: &Object, should_estimate_requires: bool, ) -> Result { let mut cost = 0.0; + let ctx = ScoringContext { + schema, + query, + variables, + should_estimate_requires, + }; if let Some(op) = &query.operations.anonymous { - cost += self.score_operation(op, schema, query, should_estimate_requires)?; + cost += self.score_operation(op, &ctx)?; } for (_name, op) in query.operations.named.iter() { - cost += self.score_operation(op, schema, query, should_estimate_requires)?; + cost += self.score_operation(op, &ctx)?; } Ok(cost) } - pub(crate) fn planned(&self, query_plan: &QueryPlan) -> Result { - self.score_plan_node(&query_plan.root) + pub(crate) fn planned( + &self, + query_plan: &QueryPlan, + variables: &Object, + ) -> Result { + self.score_plan_node(&query_plan.root, variables) } pub(crate) fn actual( &self, request: &ExecutableDocument, response: &Response, + variables: &Object, ) -> Result { let mut visitor = ResponseCostCalculator::new(&self.supergraph_schema); - visitor.visit(request, response); + visitor.visit(request, response, variables); Ok(visitor.cost) } } @@ -505,11 +517,12 @@ impl<'schema> ResponseVisitor for ResponseCostCalculator<'schema> { fn visit_field( &mut self, request: &ExecutableDocument, + variables: &Object, parent_ty: &NamedType, field: &Field, value: &Value, ) { - self.visit_list_item(request, parent_ty, field, value); + self.visit_list_item(request, variables, parent_ty, field, value); let definition = self.schema.type_field(parent_ty, &field.name); for argument in &field.arguments { @@ -517,7 +530,8 @@ impl<'schema> ResponseVisitor for ResponseCostCalculator<'schema> { .as_ref() .map(|def| def.argument_by_name(&argument.name)) { - if let Ok(score) = score_argument(&argument.value, argument_definition, self.schema) + if let Ok(score) = + score_argument(&argument.value, argument_definition, self.schema, variables) { self.cost += score; } @@ -534,6 +548,7 @@ impl<'schema> ResponseVisitor for ResponseCostCalculator<'schema> { fn visit_list_item( &mut self, request: &apollo_compiler::ExecutableDocument, + variables: &Object, parent_ty: &apollo_compiler::executable::NamedType, field: &apollo_compiler::executable::Field, value: &Value, @@ -548,12 +563,12 @@ impl<'schema> ResponseVisitor for ResponseCostCalculator<'schema> { } Value::Array(items) => { for item in items { - self.visit_list_item(request, parent_ty, field, item); + self.visit_list_item(request, variables, parent_ty, field, item); } } Value::Object(children) => { self.cost += cost_directive.map_or(1.0, |cost| cost.weight()); - self.visit_selections(request, &field.selection_set, children); + self.visit_selections(request, variables, &field.selection_set, children); } } } @@ -570,6 +585,7 @@ mod tests { use tower::Service; use super::*; + use crate::introspection::default_cache_storage; use crate::query_planner::BridgeQueryPlanner; use crate::services::layers::query_analysis::ParsedDocument; use crate::services::QueryPlannerContent; @@ -583,9 +599,10 @@ mod tests { fn rust_planned( &self, query_plan: &apollo_federation::query_plan::QueryPlan, + variables: &Object, ) -> Result { let js_planner_node: PlanNode = query_plan.node.as_ref().unwrap().into(); - self.score_plan_node(&js_planner_node) + self.score_plan_node(&js_planner_node, variables) } } @@ -600,20 +617,30 @@ mod tests { } /// Estimate cost of an operation executed on a supergraph. - fn estimated_cost(schema_str: &str, query_str: &str) -> f64 { + fn estimated_cost(schema_str: &str, query_str: &str, variables_str: &str) -> f64 { let (schema, query) = parse_schema_and_operation(schema_str, query_str, &Default::default()); + let variables = serde_json::from_str::(variables_str) + .unwrap() + .as_object() + .cloned() + .unwrap_or_default(); let schema = DemandControlledSchema::new(Arc::new(schema.supergraph_schema().clone())).unwrap(); let calculator = StaticCostCalculator::new(Arc::new(schema), Default::default(), 100); calculator - .estimated(&query.executable, &calculator.supergraph_schema, true) + .estimated( + &query.executable, + &calculator.supergraph_schema, + &variables, + true, + ) .unwrap() } /// Estimate cost of an operation on a plain, non-federated schema. - fn basic_estimated_cost(schema_str: &str, query_str: &str) -> f64 { + fn basic_estimated_cost(schema_str: &str, query_str: &str, variables_str: &str) -> f64 { let schema = apollo_compiler::Schema::parse_and_validate(schema_str, "schema.graphqls").unwrap(); let query = apollo_compiler::ExecutableDocument::parse_and_validate( @@ -622,22 +649,38 @@ mod tests { "query.graphql", ) .unwrap(); + let variables = serde_json::from_str::(variables_str) + .unwrap() + .as_object() + .cloned() + .unwrap_or_default(); let schema = DemandControlledSchema::new(Arc::new(schema)).unwrap(); let calculator = StaticCostCalculator::new(Arc::new(schema), Default::default(), 100); calculator - .estimated(&query, &calculator.supergraph_schema, true) + .estimated(&query, &calculator.supergraph_schema, &variables, true) .unwrap() } - async fn planned_cost_js(schema_str: &str, query_str: &str) -> f64 { + async fn planned_cost_js(schema_str: &str, query_str: &str, variables_str: &str) -> f64 { let config: Arc = Arc::new(Default::default()); let (schema, query) = parse_schema_and_operation(schema_str, query_str, &config); + let variables = serde_json::from_str::(variables_str) + .unwrap() + .as_object() + .cloned() + .unwrap_or_default(); let supergraph_schema = schema.supergraph_schema().clone(); - let mut planner = BridgeQueryPlanner::new(schema.into(), config.clone(), None, None) - .await - .unwrap(); + let mut planner = BridgeQueryPlanner::new( + schema.into(), + config.clone(), + None, + None, + default_cache_storage().await, + ) + .await + .unwrap(); let ctx = Context::new(); ctx.extensions() @@ -667,17 +710,24 @@ mod tests { 100, ); - calculator.planned(&query_plan).unwrap() + calculator.planned(&query_plan, &variables).unwrap() } - fn planned_cost_rust(schema_str: &str, query_str: &str) -> f64 { + fn planned_cost_rust(schema_str: &str, query_str: &str, variables_str: &str) -> f64 { let config: Arc = Arc::new(Default::default()); let (schema, query) = parse_schema_and_operation(schema_str, query_str, &config); + let variables = serde_json::from_str::(variables_str) + .unwrap() + .as_object() + .cloned() + .unwrap_or_default(); let planner = QueryPlanner::new(schema.federation_supergraph(), Default::default()).unwrap(); - let query_plan = planner.build_query_plan(&query.executable, None).unwrap(); + let query_plan = planner + .build_query_plan(&query.executable, None, Default::default()) + .unwrap(); let schema = DemandControlledSchema::new(Arc::new(schema.supergraph_schema().clone())).unwrap(); @@ -695,22 +745,37 @@ mod tests { 100, ); - calculator.rust_planned(&query_plan).unwrap() + calculator.rust_planned(&query_plan, &variables).unwrap() } - fn actual_cost(schema_str: &str, query_str: &str, response_bytes: &'static [u8]) -> f64 { + fn actual_cost( + schema_str: &str, + query_str: &str, + variables_str: &str, + response_bytes: &'static [u8], + ) -> f64 { let (schema, query) = parse_schema_and_operation(schema_str, query_str, &Default::default()); + let variables = serde_json::from_str::(variables_str) + .unwrap() + .as_object() + .cloned() + .unwrap_or_default(); let response = Response::from_bytes("test", Bytes::from(response_bytes)).unwrap(); let schema = DemandControlledSchema::new(Arc::new(schema.supergraph_schema().clone())).unwrap(); StaticCostCalculator::new(Arc::new(schema), Default::default(), 100) - .actual(&query.executable, &response) + .actual(&query.executable, &response, &variables) .unwrap() } /// Actual cost of an operation on a plain, non-federated schema. - fn basic_actual_cost(schema_str: &str, query_str: &str, response_bytes: &'static [u8]) -> f64 { + fn basic_actual_cost( + schema_str: &str, + query_str: &str, + variables_str: &str, + response_bytes: &'static [u8], + ) -> f64 { let schema = apollo_compiler::Schema::parse_and_validate(schema_str, "schema.graphqls").unwrap(); let query = apollo_compiler::ExecutableDocument::parse_and_validate( @@ -719,11 +784,16 @@ mod tests { "query.graphql", ) .unwrap(); + let variables = serde_json::from_str::(variables_str) + .unwrap() + .as_object() + .cloned() + .unwrap_or_default(); let response = Response::from_bytes("test", Bytes::from(response_bytes)).unwrap(); let schema = DemandControlledSchema::new(Arc::new(schema)).unwrap(); StaticCostCalculator::new(Arc::new(schema), Default::default(), 100) - .actual(&query, &response) + .actual(&query, &response, &variables) .unwrap() } @@ -731,157 +801,174 @@ mod tests { fn query_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 0.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 0.0) } #[test] fn mutation_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_mutation.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 10.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 10.0) } #[test] fn object_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_object_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 1.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 1.0) } #[test] fn interface_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_interface_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 1.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 1.0) } #[test] fn union_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_union_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 1.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 1.0) } #[test] fn list_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_object_list_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 100.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 100.0) } #[test] fn scalar_list_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_scalar_list_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 0.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 0.0) } #[test] fn nested_object_lists() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_nested_list_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 10100.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 10100.0) } #[test] fn input_object_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_input_object_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 4.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 4.0) } #[test] fn input_object_cost_with_returned_objects() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_input_object_query_2.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/basic_input_object_response.json"); - assert_eq!(basic_estimated_cost(schema, query), 104.0); + assert_eq!(basic_estimated_cost(schema, query, variables), 104.0); // The cost of the arguments from the query should be included when scoring the response - assert_eq!(basic_actual_cost(schema, query, response), 7.0); + assert_eq!(basic_actual_cost(schema, query, variables, response), 7.0); } #[test] fn skip_directive_excludes_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_skipped_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 0.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 0.0) } #[test] fn include_directive_excludes_cost() { let schema = include_str!("./fixtures/basic_schema.graphql"); let query = include_str!("./fixtures/basic_excluded_query.graphql"); + let variables = "{}"; - assert_eq!(basic_estimated_cost(schema, query), 0.0) + assert_eq!(basic_estimated_cost(schema, query, variables), 0.0) } #[test(tokio::test)] async fn federated_query_with_name() { let schema = include_str!("./fixtures/federated_ships_schema.graphql"); let query = include_str!("./fixtures/federated_ships_named_query.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/federated_ships_named_response.json"); - assert_eq!(estimated_cost(schema, query), 100.0); - assert_eq!(actual_cost(schema, query, response), 2.0); + assert_eq!(estimated_cost(schema, query, variables), 100.0); + assert_eq!(actual_cost(schema, query, variables, response), 2.0); } #[test(tokio::test)] async fn federated_query_with_requires() { let schema = include_str!("./fixtures/federated_ships_schema.graphql"); let query = include_str!("./fixtures/federated_ships_required_query.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/federated_ships_required_response.json"); - assert_eq!(estimated_cost(schema, query), 10200.0); - assert_eq!(planned_cost_js(schema, query).await, 10400.0); - assert_eq!(planned_cost_rust(schema, query), 10400.0); - assert_eq!(actual_cost(schema, query, response), 2.0); + assert_eq!(estimated_cost(schema, query, variables), 10200.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 10400.0); + assert_eq!(planned_cost_rust(schema, query, variables), 10400.0); + assert_eq!(actual_cost(schema, query, variables, response), 2.0); } #[test(tokio::test)] async fn federated_query_with_fragments() { let schema = include_str!("./fixtures/federated_ships_schema.graphql"); let query = include_str!("./fixtures/federated_ships_fragment_query.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/federated_ships_fragment_response.json"); - assert_eq!(estimated_cost(schema, query), 300.0); - assert_eq!(planned_cost_js(schema, query).await, 400.0); - assert_eq!(planned_cost_rust(schema, query), 400.0); - assert_eq!(actual_cost(schema, query, response), 6.0); + assert_eq!(estimated_cost(schema, query, variables), 300.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 400.0); + assert_eq!(planned_cost_rust(schema, query, variables), 400.0); + assert_eq!(actual_cost(schema, query, variables, response), 6.0); } #[test(tokio::test)] async fn federated_query_with_inline_fragments() { let schema = include_str!("./fixtures/federated_ships_schema.graphql"); let query = include_str!("./fixtures/federated_ships_inline_fragment_query.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/federated_ships_fragment_response.json"); - assert_eq!(estimated_cost(schema, query), 300.0); - assert_eq!(planned_cost_js(schema, query).await, 400.0); - assert_eq!(planned_cost_rust(schema, query), 400.0); - assert_eq!(actual_cost(schema, query, response), 6.0); + assert_eq!(estimated_cost(schema, query, variables), 300.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 400.0); + assert_eq!(planned_cost_rust(schema, query, variables), 400.0); + assert_eq!(actual_cost(schema, query, variables, response), 6.0); } #[test(tokio::test)] async fn federated_query_with_defer() { let schema = include_str!("./fixtures/federated_ships_schema.graphql"); let query = include_str!("./fixtures/federated_ships_deferred_query.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/federated_ships_deferred_response.json"); - assert_eq!(estimated_cost(schema, query), 10200.0); - assert_eq!(planned_cost_js(schema, query).await, 10400.0); - assert_eq!(planned_cost_rust(schema, query), 10400.0); - assert_eq!(actual_cost(schema, query, response), 2.0); + assert_eq!(estimated_cost(schema, query, variables), 10200.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 10400.0); + assert_eq!(planned_cost_rust(schema, query, variables), 10400.0); + assert_eq!(actual_cost(schema, query, variables, response), 2.0); } #[test(tokio::test)] @@ -895,12 +982,22 @@ mod tests { let calculator = StaticCostCalculator::new(schema.clone(), Default::default(), 100); let conservative_estimate = calculator - .estimated(&query.executable, &calculator.supergraph_schema, true) + .estimated( + &query.executable, + &calculator.supergraph_schema, + &Default::default(), + true, + ) .unwrap(); let calculator = StaticCostCalculator::new(schema.clone(), Default::default(), 5); let narrow_estimate = calculator - .estimated(&query.executable, &calculator.supergraph_schema, true) + .estimated( + &query.executable, + &calculator.supergraph_schema, + &Default::default(), + true, + ) .unwrap(); assert_eq!(conservative_estimate, 10200.0); @@ -911,24 +1008,26 @@ mod tests { async fn custom_cost_query() { let schema = include_str!("./fixtures/custom_cost_schema.graphql"); let query = include_str!("./fixtures/custom_cost_query.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/custom_cost_response.json"); - assert_eq!(estimated_cost(schema, query), 127.0); - assert_eq!(planned_cost_js(schema, query).await, 127.0); - assert_eq!(planned_cost_rust(schema, query), 127.0); - assert_eq!(actual_cost(schema, query, response), 125.0); + assert_eq!(estimated_cost(schema, query, variables), 127.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 127.0); + assert_eq!(planned_cost_rust(schema, query, variables), 127.0); + assert_eq!(actual_cost(schema, query, variables, response), 125.0); } #[test(tokio::test)] async fn custom_cost_query_with_renamed_directives() { let schema = include_str!("./fixtures/custom_cost_schema_with_renamed_directives.graphql"); let query = include_str!("./fixtures/custom_cost_query.graphql"); + let variables = "{}"; let response = include_bytes!("./fixtures/custom_cost_response.json"); - assert_eq!(estimated_cost(schema, query), 127.0); - assert_eq!(planned_cost_js(schema, query).await, 127.0); - assert_eq!(planned_cost_rust(schema, query), 127.0); - assert_eq!(actual_cost(schema, query, response), 125.0); + assert_eq!(estimated_cost(schema, query, variables), 127.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 127.0); + assert_eq!(planned_cost_rust(schema, query, variables), 127.0); + assert_eq!(actual_cost(schema, query, variables, response), 125.0); } #[test(tokio::test)] @@ -936,11 +1035,26 @@ mod tests { let schema = include_str!("./fixtures/custom_cost_schema.graphql"); let query = include_str!("./fixtures/custom_cost_query_with_default_slicing_argument.graphql"); + let variables = "{}"; + let response = include_bytes!("./fixtures/custom_cost_response.json"); + + assert_eq!(estimated_cost(schema, query, variables), 132.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 132.0); + assert_eq!(planned_cost_rust(schema, query, variables), 132.0); + assert_eq!(actual_cost(schema, query, variables, response), 125.0); + } + + #[test(tokio::test)] + async fn custom_cost_query_with_variable_slicing_argument() { + let schema = include_str!("./fixtures/custom_cost_schema.graphql"); + let query = + include_str!("./fixtures/custom_cost_query_with_variable_slicing_argument.graphql"); + let variables = r#"{"costlyInput": {"somethingWithCost": 10}, "fieldCountVar": 5}"#; let response = include_bytes!("./fixtures/custom_cost_response.json"); - assert_eq!(estimated_cost(schema, query), 132.0); - assert_eq!(planned_cost_js(schema, query).await, 132.0); - assert_eq!(planned_cost_rust(schema, query), 132.0); - assert_eq!(actual_cost(schema, query, response), 125.0); + assert_eq!(estimated_cost(schema, query, variables), 127.0); + assert_eq!(planned_cost_js(schema, query, variables).await, 127.0); + assert_eq!(planned_cost_rust(schema, query, variables), 127.0); + assert_eq!(actual_cost(schema, query, variables, response), 125.0); } } diff --git a/apollo-router/src/plugins/demand_control/mod.rs b/apollo-router/src/plugins/demand_control/mod.rs index b3faaef747..62255fa832 100644 --- a/apollo-router/src/plugins/demand_control/mod.rs +++ b/apollo-router/src/plugins/demand_control/mod.rs @@ -42,36 +42,11 @@ use crate::Context; pub(crate) mod cost_calculator; pub(crate) mod strategy; -/// The cost calculation information stored in context for use in telemetry and other plugins that need to know what cost was calculated. -#[derive(Debug, Clone)] -pub(crate) struct CostContext { - pub(crate) estimated: f64, - pub(crate) actual: f64, - pub(crate) result: &'static str, - pub(crate) strategy: &'static str, -} - -impl Default for CostContext { - fn default() -> Self { - Self { - estimated: 0.0, - actual: 0.0, - result: "COST_OK", - strategy: "COST_STRATEGY_UNKNOWN", - } - } -} - -impl CostContext { - pub(crate) fn delta(&self) -> f64 { - self.estimated - self.actual - } - - pub(crate) fn result(&mut self, error: DemandControlError) -> DemandControlError { - self.result = error.code(); - error - } -} +pub(crate) static COST_ESTIMATED_KEY: &str = "cost.estimated"; +pub(crate) static COST_ACTUAL_KEY: &str = "cost.actual"; +pub(crate) static COST_DELTA_KEY: &str = "cost.delta"; +pub(crate) static COST_RESULT_KEY: &str = "cost.result"; +pub(crate) static COST_STRATEGY_KEY: &str = "cost.strategy"; /// Algorithm for calculating the cost of an incoming query. #[derive(Clone, Debug, Deserialize, JsonSchema)] @@ -146,6 +121,8 @@ pub(crate) enum DemandControlError { QueryParseFailure(String), /// {0} SubgraphOperationNotInitialized(crate::query_planner::fetch::SubgraphOperationNotInitialized), + /// {0} + ContextSerializationError(String), } impl IntoGraphQLErrors for DemandControlError { @@ -182,6 +159,10 @@ impl IntoGraphQLErrors for DemandControlError { .message(self.to_string()) .build()]), DemandControlError::SubgraphOperationNotInitialized(e) => Ok(e.into_graphql_errors()), + DemandControlError::ContextSerializationError(_) => Ok(vec![graphql::Error::builder() + .extension_code(self.code()) + .message(self.to_string()) + .build()]), } } } @@ -193,6 +174,7 @@ impl DemandControlError { DemandControlError::ActualCostTooExpensive { .. } => "COST_ACTUAL_TOO_EXPENSIVE", DemandControlError::QueryParseFailure(_) => "COST_QUERY_PARSE_FAILURE", DemandControlError::SubgraphOperationNotInitialized(e) => e.code(), + DemandControlError::ContextSerializationError(_) => "COST_CONTEXT_SERIALIZATION_ERROR", } } } @@ -219,6 +201,72 @@ impl<'a> From> for DemandControlError { } } +#[derive(Clone)] +pub(crate) struct DemandControlContext { + pub(crate) strategy: Strategy, + pub(crate) variables: Object, +} + +impl Context { + pub(crate) fn insert_estimated_cost(&self, cost: f64) -> Result<(), DemandControlError> { + self.insert(COST_ESTIMATED_KEY, cost) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string()))?; + Ok(()) + } + + pub(crate) fn get_estimated_cost(&self) -> Result, DemandControlError> { + self.get::<&str, f64>(COST_ESTIMATED_KEY) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string())) + } + + pub(crate) fn insert_actual_cost(&self, cost: f64) -> Result<(), DemandControlError> { + self.insert(COST_ACTUAL_KEY, cost) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string()))?; + Ok(()) + } + + pub(crate) fn get_actual_cost(&self) -> Result, DemandControlError> { + self.get::<&str, f64>(COST_ACTUAL_KEY) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string())) + } + + pub(crate) fn get_cost_delta(&self) -> Result, DemandControlError> { + let estimated = self.get_estimated_cost()?; + let actual = self.get_actual_cost()?; + Ok(estimated.zip(actual).map(|(est, act)| est - act)) + } + + pub(crate) fn insert_cost_result(&self, result: String) -> Result<(), DemandControlError> { + self.insert(COST_RESULT_KEY, result) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string()))?; + Ok(()) + } + + pub(crate) fn get_cost_result(&self) -> Result, DemandControlError> { + self.get::<&str, String>(COST_RESULT_KEY) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string())) + } + + pub(crate) fn insert_cost_strategy(&self, strategy: String) -> Result<(), DemandControlError> { + self.insert(COST_STRATEGY_KEY, strategy) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string()))?; + Ok(()) + } + + pub(crate) fn get_cost_strategy(&self) -> Result, DemandControlError> { + self.get::<&str, String>(COST_STRATEGY_KEY) + .map_err(|e| DemandControlError::ContextSerializationError(e.to_string())) + } + + pub(crate) fn insert_demand_control_context(&self, ctx: DemandControlContext) { + self.extensions().with_lock(|mut lock| lock.insert(ctx)); + } + + pub(crate) fn get_demand_control_context(&self) -> Option { + self.extensions().with_lock(|lock| lock.get().cloned()) + } +} + pub(crate) struct DemandControl { config: DemandControlConfig, strategy_factory: StrategyFactory, @@ -227,8 +275,10 @@ pub(crate) struct DemandControl { impl DemandControl { fn report_operation_metric(context: Context) { let result = context - .extensions() - .with_lock(|lock| lock.get::().map_or("NO_CONTEXT", |c| c.result)); + .get(COST_RESULT_KEY) + .ok() + .flatten() + .unwrap_or("NO_CONTEXT".to_string()); u64_counter!( "apollo.router.operations.demand_control", "Total operations with demand control enabled", @@ -271,8 +321,11 @@ impl Plugin for DemandControl { ServiceBuilder::new() .checkpoint(move |req: execution::Request| { req.context - .extensions() - .with_lock(|mut lock| lock.insert(strategy.clone())); + .insert_demand_control_context(DemandControlContext { + strategy: strategy.clone(), + variables: req.supergraph_request.body().variables.clone(), + }); + // On the request path we need to check for estimates, checkpoint is used to do this, short-circuiting the request if it's too expensive. Ok(match strategy.on_execution_request(&req) { Ok(_) => ControlFlow::Continue(req), @@ -293,9 +346,11 @@ impl Plugin for DemandControl { .context .unsupported_executable_document() .expect("must have document"); - let strategy = resp.context.extensions().with_lock(|lock| { - lock.get::().expect("must have strategy").clone() - }); + let strategy = resp + .context + .get_demand_control_context() + .map(|ctx| ctx.strategy) + .expect("must have strategy"); let context = resp.context.clone(); // We want to sequence this code to run after all the subgraph responses have been scored. @@ -356,9 +411,7 @@ impl Plugin for DemandControl { let subgraph_name_map_fut = subgraph_name.to_owned(); ServiceBuilder::new() .checkpoint(move |req: subgraph::Request| { - let strategy = req.context.extensions().with_lock(|lock| { - lock.get::().expect("must have strategy").clone() - }); + let strategy = req.context.get_demand_control_context().map(|c| c.strategy).expect("must have strategy"); // On the request path we need to check for estimates, checkpoint is used to do this, short-circuiting the request if it's too expensive. Ok(match strategy.on_subgraph_request(&req) { @@ -388,9 +441,7 @@ impl Plugin for DemandControl { }, |(subgraph_name, req): (String, Arc>), fut| async move { let resp: subgraph::Response = fut.await?; - let strategy = resp.context.extensions().with_lock(|lock| { - lock.get::().expect("must have strategy").clone() - }); + let strategy = resp.context.get_demand_control_context().map(|c| c.strategy).expect("must have strategy"); Ok(match strategy.on_subgraph_response(req.as_ref(), &resp) { Ok(_) => resp, Err(err) => subgraph::Response::builder() @@ -428,6 +479,7 @@ mod test { use crate::graphql::Response; use crate::metrics::FutureMetricsExt; use crate::plugins::demand_control::DemandControl; + use crate::plugins::demand_control::DemandControlContext; use crate::plugins::demand_control::DemandControlError; use crate::plugins::test::PluginTestHarness; use crate::query_planner::fetch::QueryHash; @@ -586,7 +638,10 @@ mod test { let strategy = plugin.strategy_factory.create(); let ctx = context(); - ctx.extensions().with_lock(|mut lock| lock.insert(strategy)); + ctx.insert_demand_control_context(DemandControlContext { + strategy, + variables: Default::default(), + }); let mut req = subgraph::Request::fake_builder() .subgraph_name("test") .context(ctx) diff --git a/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs b/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs index 41de6926b4..f3a114e8fb 100644 --- a/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs +++ b/apollo-router/src/plugins/demand_control/strategy/static_estimated.rs @@ -3,7 +3,6 @@ use apollo_compiler::ExecutableDocument; use crate::graphql; use crate::plugins::demand_control::cost_calculator::static_cost::StaticCostCalculator; use crate::plugins::demand_control::strategy::StrategyImpl; -use crate::plugins::demand_control::CostContext; use crate::plugins::demand_control::DemandControlError; use crate::services::execution; use crate::services::subgraph; @@ -18,23 +17,29 @@ pub(crate) struct StaticEstimated { impl StrategyImpl for StaticEstimated { fn on_execution_request(&self, request: &execution::Request) -> Result<(), DemandControlError> { self.cost_calculator - .planned(&request.query_plan) + .planned( + &request.query_plan, + &request.supergraph_request.body().variables, + ) .and_then(|cost| { - request.context.extensions().with_lock(|mut lock| { - let cost_result = lock.get_or_default_mut::(); - cost_result.strategy = "static_estimated"; - cost_result.estimated = cost; - if cost > self.max { - Err( - cost_result.result(DemandControlError::EstimatedCostTooExpensive { - estimated_cost: cost, - max_cost: self.max, - }), - ) - } else { - Ok(()) - } - }) + request + .context + .insert_cost_strategy("static_estimated".to_string())?; + request.context.insert_cost_result("COST_OK".to_string())?; + request.context.insert_estimated_cost(cost)?; + + if cost > self.max { + let error = DemandControlError::EstimatedCostTooExpensive { + estimated_cost: cost, + max_cost: self.max, + }; + request + .context + .insert_cost_result(error.code().to_string())?; + Err(error) + } else { + Ok(()) + } }) } @@ -57,10 +62,15 @@ impl StrategyImpl for StaticEstimated { response: &graphql::Response, ) -> Result<(), DemandControlError> { if response.data.is_some() { - let cost = self.cost_calculator.actual(request, response)?; - context - .extensions() - .with_lock(|mut lock| lock.get_or_default_mut::().actual = cost); + let cost = self.cost_calculator.actual( + request, + response, + &context + .extensions() + .with_lock(|lock| lock.get().cloned()) + .unwrap_or_default(), + )?; + context.insert_actual_cost(cost)?; } Ok(()) } diff --git a/apollo-router/src/plugins/demand_control/strategy/test.rs b/apollo-router/src/plugins/demand_control/strategy/test.rs index 347f77d79f..265613472d 100644 --- a/apollo-router/src/plugins/demand_control/strategy/test.rs +++ b/apollo-router/src/plugins/demand_control/strategy/test.rs @@ -3,7 +3,6 @@ use apollo_compiler::ExecutableDocument; use crate::plugins::demand_control::strategy::StrategyImpl; use crate::plugins::demand_control::test::TestError; use crate::plugins::demand_control::test::TestStage; -use crate::plugins::demand_control::CostContext; use crate::plugins::demand_control::DemandControlError; use crate::services::execution::Request; use crate::services::subgraph::Response; @@ -17,32 +16,38 @@ pub(crate) struct Test { impl StrategyImpl for Test { fn on_execution_request(&self, request: &Request) -> Result<(), DemandControlError> { - request.context.extensions().with_lock(|mut lock| { - let cost_context = lock.get_or_default_mut::(); - match self { - Test { - stage: TestStage::ExecutionRequest, - error, - } => Err(cost_context.result(error.into())), - _ => Ok(()), + match self { + Test { + stage: TestStage::ExecutionRequest, + error, + } => { + let error: DemandControlError = error.into(); + request + .context + .insert_cost_result(error.code().to_string())?; + Err(error) } - }) + _ => Ok(()), + } } fn on_subgraph_request( &self, request: &crate::services::subgraph::Request, ) -> Result<(), DemandControlError> { - request.context.extensions().with_lock(|mut lock| { - let cost_context = lock.get_or_default_mut::(); - match self { - Test { - stage: TestStage::SubgraphRequest, - error, - } => Err(cost_context.result(error.into())), - _ => Ok(()), + match self { + Test { + stage: TestStage::SubgraphRequest, + error, + } => { + let error: DemandControlError = error.into(); + request + .context + .insert_cost_result(error.code().to_string())?; + Err(error) } - }) + _ => Ok(()), + } } fn on_subgraph_response( @@ -50,16 +55,19 @@ impl StrategyImpl for Test { _request: &ExecutableDocument, response: &Response, ) -> Result<(), DemandControlError> { - response.context.extensions().with_lock(|mut lock| { - let cost_context = lock.get_or_default_mut::(); - match self { - Test { - stage: TestStage::SubgraphResponse, - error, - } => Err(cost_context.result(error.into())), - _ => Ok(()), + match self { + Test { + stage: TestStage::SubgraphResponse, + error, + } => { + let error: DemandControlError = error.into(); + response + .context + .insert_cost_result(error.code().to_string())?; + Err(error) } - }) + _ => Ok(()), + } } fn on_execution_response( @@ -68,15 +76,16 @@ impl StrategyImpl for Test { _request: &ExecutableDocument, _response: &crate::graphql::Response, ) -> Result<(), DemandControlError> { - context.extensions().with_lock(|mut lock| { - let cost_context = lock.get_or_default_mut::(); - match self { - Test { - stage: TestStage::ExecutionResponse, - error, - } => Err(cost_context.result(error.into())), - _ => Ok(()), + match self { + Test { + stage: TestStage::ExecutionResponse, + error, + } => { + let error: DemandControlError = error.into(); + context.insert_cost_result(error.code().to_string())?; + Err(error) } - }) + _ => Ok(()), + } } } diff --git a/apollo-router/src/plugins/include_subgraph_errors.rs b/apollo-router/src/plugins/include_subgraph_errors.rs index 4f558ced82..34348f4ef7 100644 --- a/apollo-router/src/plugins/include_subgraph_errors.rs +++ b/apollo-router/src/plugins/include_subgraph_errors.rs @@ -104,7 +104,7 @@ mod test { use crate::Configuration; static UNREDACTED_PRODUCT_RESPONSE: Lazy = Lazy::new(|| { - Bytes::from_static(r#"{"data":{"topProducts":null},"errors":[{"message":"couldn't find mock for query {\"query\":\"query ErrorTopProducts__products__0($first:Int){topProducts(first:$first){__typename upc name}}\",\"operationName\":\"ErrorTopProducts__products__0\",\"variables\":{\"first\":2}}","path":[],"extensions":{"test":"value","code":"FETCH_ERROR"}}]}"#.as_bytes()) + Bytes::from_static(r#"{"data":{"topProducts":null},"errors":[{"message":"couldn't find mock for query {\"query\":\"query($first: Int) { topProducts(first: $first) { __typename upc } }\",\"variables\":{\"first\":2}}","path":[],"extensions":{"test":"value","code":"FETCH_ERROR"}}]}"#.as_bytes()) }); static REDACTED_PRODUCT_RESPONSE: Lazy = Lazy::new(|| { @@ -127,7 +127,7 @@ mod test { static VALID_QUERY: &str = r#"query TopProducts($first: Int) { topProducts(first: $first) { upc name reviews { id product { name } author { id name } } } }"#; - static ERROR_PRODUCT_QUERY: &str = r#"query ErrorTopProducts($first: Int) { topProducts(first: $first) { upc name reviews { id product { name } author { id name } } } }"#; + static ERROR_PRODUCT_QUERY: &str = r#"query ErrorTopProducts($first: Int) { topProducts(first: $first) { upc reviews { id product { name } author { id name } } } }"#; static ERROR_ACCOUNT_QUERY: &str = r#"query Query { me { name }}"#; @@ -195,6 +195,7 @@ mod test { include_str!("../../../apollo-router-benchmarks/benches/fixtures/supergraph.graphql"); let schema = Schema::parse(schema, &Default::default()).unwrap(); let planner = BridgeQueryPlannerPool::new( + Vec::new(), schema.into(), Default::default(), NonZeroUsize::new(1).unwrap(), diff --git a/apollo-router/src/plugins/rhai/engine.rs b/apollo-router/src/plugins/rhai/engine.rs index da87655c17..062143711f 100644 --- a/apollo-router/src/plugins/rhai/engine.rs +++ b/apollo-router/src/plugins/rhai/engine.rs @@ -46,6 +46,10 @@ use crate::graphql::Response; use crate::http_ext; use crate::plugins::authentication::APOLLO_AUTHENTICATION_JWT_CLAIMS; use crate::plugins::cache::entity::CONTEXT_CACHE_KEY; +use crate::plugins::demand_control::COST_ACTUAL_KEY; +use crate::plugins::demand_control::COST_ESTIMATED_KEY; +use crate::plugins::demand_control::COST_RESULT_KEY; +use crate::plugins::demand_control::COST_STRATEGY_KEY; use crate::plugins::subscription::SUBSCRIPTION_WS_CUSTOM_CONNECTION_PARAMS; use crate::query_planner::APOLLO_OPERATION_ID; use crate::Context; @@ -1777,6 +1781,14 @@ impl Rhai { ); global_variables.insert("APOLLO_ENTITY_CACHE_KEY".into(), CONTEXT_CACHE_KEY.into()); global_variables.insert("APOLLO_OPERATION_ID".into(), APOLLO_OPERATION_ID.into()); + // Demand Control Context Keys + global_variables.insert( + "APOLLO_COST_ESTIMATED_KEY".into(), + COST_ESTIMATED_KEY.into(), + ); + global_variables.insert("APOLLO_COST_ACTUAL_KEY".into(), COST_ACTUAL_KEY.into()); + global_variables.insert("APOLLO_COST_STRATEGY_KEY".into(), COST_STRATEGY_KEY.into()); + global_variables.insert("APOLLO_COST_RESULT_KEY".into(), COST_RESULT_KEY.into()); let shared_globals = Arc::new(global_variables); diff --git a/apollo-router/src/plugins/rhai/tests.rs b/apollo-router/src/plugins/rhai/tests.rs index b47c25774d..11c4f3a03a 100644 --- a/apollo-router/src/plugins/rhai/tests.rs +++ b/apollo-router/src/plugins/rhai/tests.rs @@ -785,3 +785,63 @@ async fn test_router_service_adds_timestamp_header() -> Result<(), BoxError> { Ok(()) } + +#[tokio::test] +async fn it_can_access_demand_control_context() -> Result<(), BoxError> { + let mut mock_service = MockSupergraphService::new(); + mock_service + .expect_call() + .times(1) + .returning(move |req: SupergraphRequest| { + Ok(SupergraphResponse::fake_builder() + .context(req.context) + .build() + .unwrap()) + }); + + let dyn_plugin: Box = crate::plugin::plugins() + .find(|factory| factory.name == "apollo.rhai") + .expect("Plugin not found") + .create_instance_without_schema( + &Value::from_str(r#"{"scripts":"tests/fixtures", "main":"demand_control.rhai"}"#) + .unwrap(), + ) + .await + .unwrap(); + + let mut router_service = dyn_plugin.supergraph_service(BoxService::new(mock_service)); + let context = Context::new(); + context.insert_estimated_cost(50.0).unwrap(); + context.insert_actual_cost(35.0).unwrap(); + context + .insert_cost_strategy("test_strategy".to_string()) + .unwrap(); + context.insert_cost_result("COST_OK".to_string()).unwrap(); + let supergraph_req = SupergraphRequest::fake_builder().context(context).build()?; + + let service_response = router_service.ready().await?.call(supergraph_req).await?; + assert_eq!(StatusCode::OK, service_response.response.status()); + + let headers = service_response.response.headers().clone(); + let demand_control_header = headers + .get("demand-control-estimate") + .map(|h| h.to_str().unwrap()); + assert_eq!(demand_control_header, Some("50.0")); + + let demand_control_header = headers + .get("demand-control-actual") + .map(|h| h.to_str().unwrap()); + assert_eq!(demand_control_header, Some("35.0")); + + let demand_control_header = headers + .get("demand-control-strategy") + .map(|h| h.to_str().unwrap()); + assert_eq!(demand_control_header, Some("test_strategy")); + + let demand_control_header = headers + .get("demand-control-result") + .map(|h| h.to_str().unwrap()); + assert_eq!(demand_control_header, Some("COST_OK")); + + Ok(()) +} diff --git a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap index e914049664..b8130ecf59 100644 --- a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap +++ b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan-2.snap @@ -69,7 +69,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "39cac6386a951cd4dbdfc9c91d7d24cc1061481ab03b72c483422446e09cba32", + "schemaAwareHash": "c595a39efeab9494c75a29de44ec4748c1741ddb96e1833e99139b058aa9da84", "authorization": { "is_authenticated": false, "scopes": [], @@ -109,7 +109,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "ee6ac550117eed7d8fcaf66c83fd5177bf03a9d5761f484e2664ea4e66149127", + "schemaAwareHash": "7054d7662e20905b01d6f937e6b588ed422e0e79de737c98e3d51b6dc610179f", "authorization": { "is_authenticated": false, "scopes": [], @@ -200,7 +200,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "66c61f60e730b77cd0a58908fee01dc7a0742c47e9f847037e01297d37918821", + "schemaAwareHash": "bff0ce0cfd6e2830949c59ae26f350d06d76150d6041b08c3d0c4384bc20b271", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap index e914049664..b8130ecf59 100644 --- a/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap +++ b/apollo-router/src/plugins/snapshots/apollo_router__plugins__expose_query_plan__tests__it_expose_query_plan.snap @@ -69,7 +69,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "39cac6386a951cd4dbdfc9c91d7d24cc1061481ab03b72c483422446e09cba32", + "schemaAwareHash": "c595a39efeab9494c75a29de44ec4748c1741ddb96e1833e99139b058aa9da84", "authorization": { "is_authenticated": false, "scopes": [], @@ -109,7 +109,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "ee6ac550117eed7d8fcaf66c83fd5177bf03a9d5761f484e2664ea4e66149127", + "schemaAwareHash": "7054d7662e20905b01d6f937e6b588ed422e0e79de737c98e3d51b6dc610179f", "authorization": { "is_authenticated": false, "scopes": [], @@ -200,7 +200,7 @@ expression: "serde_json::to_value(response).unwrap()" "inputRewrites": null, "outputRewrites": null, "contextRewrites": null, - "schemaAwareHash": "66c61f60e730b77cd0a58908fee01dc7a0742c47e9f847037e01297d37918821", + "schemaAwareHash": "bff0ce0cfd6e2830949c59ae26f350d06d76150d6041b08c3d0c4384bc20b271", "authorization": { "is_authenticated": false, "scopes": [], diff --git a/apollo-router/src/plugins/telemetry/config_new/attributes.rs b/apollo-router/src/plugins/telemetry/config_new/attributes.rs index 5ce016cc0e..53640cd87b 100644 --- a/apollo-router/src/plugins/telemetry/config_new/attributes.rs +++ b/apollo-router/src/plugins/telemetry/config_new/attributes.rs @@ -77,6 +77,23 @@ pub(crate) enum DefaultAttributeRequirementLevel { Recommended, } +#[derive(Deserialize, JsonSchema, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields, rename_all = "snake_case", untagged)] +pub(crate) enum StandardAttribute { + Bool(bool), + Aliased { alias: String }, +} + +impl StandardAttribute { + pub(crate) fn key(&self, original_key: Key) -> Option { + match self { + StandardAttribute::Bool(true) => Some(original_key), + StandardAttribute::Aliased { alias } => Some(Key::new(alias.clone())), + _ => None, + } + } +} + #[derive(Deserialize, JsonSchema, Clone, Default, Debug)] #[cfg_attr(test, derive(PartialEq))] #[serde(deny_unknown_fields, default)] @@ -84,12 +101,11 @@ pub(crate) struct RouterAttributes { /// The datadog trace ID. /// This can be output in logs and used to correlate traces in Datadog. #[serde(rename = "dd.trace_id")] - pub(crate) datadog_trace_id: Option, + pub(crate) datadog_trace_id: Option, /// The OpenTelemetry trace ID. /// This can be output in logs. - #[serde(rename = "trace_id")] - pub(crate) trace_id: Option, + pub(crate) trace_id: Option, /// All key values from trace baggage. pub(crate) baggage: Option, @@ -124,7 +140,7 @@ pub(crate) struct SupergraphAttributes { /// /// Requirement level: Recommended #[serde(rename = "graphql.document")] - pub(crate) graphql_document: Option, + pub(crate) graphql_document: Option, /// The name of the operation being executed. /// Examples: @@ -133,7 +149,7 @@ pub(crate) struct SupergraphAttributes { /// /// Requirement level: Recommended #[serde(rename = "graphql.operation.name")] - pub(crate) graphql_operation_name: Option, + pub(crate) graphql_operation_name: Option, /// The type of the operation being executed. /// Examples: @@ -144,7 +160,7 @@ pub(crate) struct SupergraphAttributes { /// /// Requirement level: Recommended #[serde(rename = "graphql.operation.type")] - pub(crate) graphql_operation_type: Option, + pub(crate) graphql_operation_type: Option, /// Cost attributes for the operation being executed #[serde(flatten)] @@ -161,13 +177,13 @@ impl DefaultForLevel for SupergraphAttributes { DefaultAttributeRequirementLevel::Required => {} DefaultAttributeRequirementLevel::Recommended => { if self.graphql_document.is_none() { - self.graphql_document = Some(true); + self.graphql_document = Some(StandardAttribute::Bool(true)); } if self.graphql_operation_name.is_none() { - self.graphql_operation_name = Some(true); + self.graphql_operation_name = Some(StandardAttribute::Bool(true)); } if self.graphql_operation_type.is_none() { - self.graphql_operation_type = Some(true); + self.graphql_operation_type = Some(StandardAttribute::Bool(true)); } } DefaultAttributeRequirementLevel::None => {} @@ -185,7 +201,7 @@ pub(crate) struct SubgraphAttributes { /// /// Requirement level: Required #[serde(rename = "subgraph.name")] - subgraph_name: Option, + subgraph_name: Option, /// The GraphQL document being executed. /// Examples: @@ -194,7 +210,7 @@ pub(crate) struct SubgraphAttributes { /// /// Requirement level: Recommended #[serde(rename = "subgraph.graphql.document")] - graphql_document: Option, + graphql_document: Option, /// The name of the operation being executed. /// Examples: @@ -203,7 +219,7 @@ pub(crate) struct SubgraphAttributes { /// /// Requirement level: Recommended #[serde(rename = "subgraph.graphql.operation.name")] - graphql_operation_name: Option, + graphql_operation_name: Option, /// The type of the operation being executed. /// Examples: @@ -214,7 +230,7 @@ pub(crate) struct SubgraphAttributes { /// /// Requirement level: Recommended #[serde(rename = "subgraph.graphql.operation.type")] - graphql_operation_type: Option, + graphql_operation_type: Option, } impl DefaultForLevel for SubgraphAttributes { @@ -226,21 +242,21 @@ impl DefaultForLevel for SubgraphAttributes { match requirement_level { DefaultAttributeRequirementLevel::Required => { if self.subgraph_name.is_none() { - self.subgraph_name = Some(true); + self.subgraph_name = Some(StandardAttribute::Bool(true)); } } DefaultAttributeRequirementLevel::Recommended => { if self.subgraph_name.is_none() { - self.subgraph_name = Some(true); + self.subgraph_name = Some(StandardAttribute::Bool(true)); } if self.graphql_document.is_none() { - self.graphql_document = Some(true); + self.graphql_document = Some(StandardAttribute::Bool(true)); } if self.graphql_operation_name.is_none() { - self.graphql_operation_name = Some(true); + self.graphql_operation_name = Some(StandardAttribute::Bool(true)); } if self.graphql_operation_type.is_none() { - self.graphql_operation_type = Some(true); + self.graphql_operation_type = Some(StandardAttribute::Bool(true)); } } DefaultAttributeRequirementLevel::None => {} @@ -263,7 +279,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Conditionally Required: If request has ended with an error. #[serde(rename = "error.type")] - pub(crate) error_type: Option, + pub(crate) error_type: Option, /// The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. /// Examples: @@ -272,7 +288,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Recommended #[serde(rename = "http.request.body.size")] - pub(crate) http_request_body_size: Option, + pub(crate) http_request_body_size: Option, /// HTTP request method. /// Examples: @@ -283,7 +299,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Required #[serde(rename = "http.request.method")] - pub(crate) http_request_method: Option, + pub(crate) http_request_method: Option, /// Original HTTP method sent by the client in the request line. /// Examples: @@ -294,7 +310,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Conditionally Required (If and only if it’s different than http.request.method) #[serde(rename = "http.request.method.original", skip)] - pub(crate) http_request_method_original: Option, + pub(crate) http_request_method_original: Option, /// The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. /// Examples: @@ -303,7 +319,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Recommended #[serde(rename = "http.response.body.size")] - pub(crate) http_response_body_size: Option, + pub(crate) http_response_body_size: Option, /// HTTP response status code. /// Examples: @@ -312,7 +328,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Conditionally Required: If and only if one was received/sent. #[serde(rename = "http.response.status_code")] - pub(crate) http_response_status_code: Option, + pub(crate) http_response_status_code: Option, /// OSI application layer or non-OSI equivalent. /// Examples: @@ -322,7 +338,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Recommended: if not default (http). #[serde(rename = "network.protocol.name")] - pub(crate) network_protocol_name: Option, + pub(crate) network_protocol_name: Option, /// Version of the protocol specified in network.protocol.name. /// Examples: @@ -334,7 +350,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Recommended #[serde(rename = "network.protocol.version")] - pub(crate) network_protocol_version: Option, + pub(crate) network_protocol_version: Option, /// OSI transport layer. /// Examples: @@ -344,7 +360,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Conditionally Required #[serde(rename = "network.transport")] - pub(crate) network_transport: Option, + pub(crate) network_transport: Option, /// OSI network layer or non-OSI equivalent. /// Examples: @@ -354,7 +370,7 @@ pub(crate) struct HttpCommonAttributes { /// /// Requirement level: Recommended #[serde(rename = "network.type")] - pub(crate) network_type: Option, + pub(crate) network_type: Option, } impl DefaultForLevel for HttpCommonAttributes { @@ -366,13 +382,13 @@ impl DefaultForLevel for HttpCommonAttributes { match requirement_level { DefaultAttributeRequirementLevel::Required => { if self.error_type.is_none() { - self.error_type = Some(true); + self.error_type = Some(StandardAttribute::Bool(true)); } if self.http_request_method.is_none() { - self.http_request_method = Some(true); + self.http_request_method = Some(StandardAttribute::Bool(true)); } if self.http_response_status_code.is_none() { - self.http_response_status_code = Some(true); + self.http_response_status_code = Some(StandardAttribute::Bool(true)); } } DefaultAttributeRequirementLevel::Recommended => { @@ -380,21 +396,21 @@ impl DefaultForLevel for HttpCommonAttributes { match kind { TelemetryDataKind::Traces => { if self.http_request_body_size.is_none() { - self.http_request_body_size = Some(true); + self.http_request_body_size = Some(StandardAttribute::Bool(true)); } if self.http_response_body_size.is_none() { - self.http_response_body_size = Some(true); + self.http_response_body_size = Some(StandardAttribute::Bool(true)); } if self.network_protocol_version.is_none() { - self.network_protocol_version = Some(true); + self.network_protocol_version = Some(StandardAttribute::Bool(true)); } if self.network_type.is_none() { - self.network_type = Some(true); + self.network_type = Some(StandardAttribute::Bool(true)); } } TelemetryDataKind::Metrics => { if self.network_protocol_version.is_none() { - self.network_protocol_version = Some(true); + self.network_protocol_version = Some(StandardAttribute::Bool(true)); } } } @@ -417,7 +433,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Recommended #[serde(rename = "client.address", skip)] - pub(crate) client_address: Option, + pub(crate) client_address: Option, /// The port of the original client behind all proxies, if known (e.g. from Forwarded or a similar header). Otherwise, the immediate client peer port. /// Examples: /// @@ -425,7 +441,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Recommended #[serde(rename = "client.port", skip)] - pub(crate) client_port: Option, + pub(crate) client_port: Option, /// The matched route (path template in the format used by the respective server framework). /// Examples: /// @@ -433,7 +449,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Conditionally Required: If and only if it’s available #[serde(rename = "http.route")] - pub(crate) http_route: Option, + pub(crate) http_route: Option, /// Local socket address. Useful in case of a multi-IP host. /// Examples: /// @@ -442,7 +458,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Opt-In #[serde(rename = "network.local.address")] - pub(crate) network_local_address: Option, + pub(crate) network_local_address: Option, /// Local socket port. Useful in case of a multi-port host. /// Examples: /// @@ -450,7 +466,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Opt-In #[serde(rename = "network.local.port")] - pub(crate) network_local_port: Option, + pub(crate) network_local_port: Option, /// Peer address of the network connection - IP address or Unix domain socket name. /// Examples: /// @@ -459,7 +475,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Recommended #[serde(rename = "network.peer.address")] - pub(crate) network_peer_address: Option, + pub(crate) network_peer_address: Option, /// Peer port number of the network connection. /// Examples: /// @@ -467,7 +483,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Recommended #[serde(rename = "network.peer.port")] - pub(crate) network_peer_port: Option, + pub(crate) network_peer_port: Option, /// Name of the local HTTP server that received the request. /// Examples: /// @@ -477,7 +493,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Recommended #[serde(rename = "server.address")] - pub(crate) server_address: Option, + pub(crate) server_address: Option, /// Port of the local HTTP server that received the request. /// Examples: /// @@ -487,7 +503,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Recommended #[serde(rename = "server.port")] - pub(crate) server_port: Option, + pub(crate) server_port: Option, /// The URI path component /// Examples: /// @@ -495,7 +511,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Required #[serde(rename = "url.path")] - pub(crate) url_path: Option, + pub(crate) url_path: Option, /// The URI query component /// Examples: /// @@ -503,7 +519,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Conditionally Required: If and only if one was received/sent. #[serde(rename = "url.query")] - pub(crate) url_query: Option, + pub(crate) url_query: Option, /// The URI scheme component identifying the used protocol. /// Examples: @@ -513,7 +529,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Required #[serde(rename = "url.scheme")] - pub(crate) url_scheme: Option, + pub(crate) url_scheme: Option, /// Value of the HTTP User-Agent header sent by the client. /// Examples: @@ -523,7 +539,7 @@ pub(crate) struct HttpServerAttributes { /// /// Requirement level: Recommended #[serde(rename = "user_agent.original")] - pub(crate) user_agent_original: Option, + pub(crate) user_agent_original: Option, } impl DefaultForLevel for HttpServerAttributes { @@ -536,41 +552,41 @@ impl DefaultForLevel for HttpServerAttributes { DefaultAttributeRequirementLevel::Required => match kind { TelemetryDataKind::Traces => { if self.url_scheme.is_none() { - self.url_scheme = Some(true); + self.url_scheme = Some(StandardAttribute::Bool(true)); } if self.url_path.is_none() { - self.url_path = Some(true); + self.url_path = Some(StandardAttribute::Bool(true)); } if self.url_query.is_none() { - self.url_query = Some(true); + self.url_query = Some(StandardAttribute::Bool(true)); } if self.http_route.is_none() { - self.http_route = Some(true); + self.http_route = Some(StandardAttribute::Bool(true)); } } TelemetryDataKind::Metrics => { if self.server_address.is_none() { - self.server_address = Some(true); + self.server_address = Some(StandardAttribute::Bool(true)); } if self.server_port.is_none() && self.server_address.is_some() { - self.server_port = Some(true); + self.server_port = Some(StandardAttribute::Bool(true)); } } }, DefaultAttributeRequirementLevel::Recommended => match kind { TelemetryDataKind::Traces => { if self.client_address.is_none() { - self.client_address = Some(true); + self.client_address = Some(StandardAttribute::Bool(true)); } if self.server_address.is_none() { - self.server_address = Some(true); + self.server_address = Some(StandardAttribute::Bool(true)); } if self.server_port.is_none() && self.server_address.is_some() { - self.server_port = Some(true); + self.server_port = Some(StandardAttribute::Bool(true)); } if self.user_agent_original.is_none() { - self.user_agent_original = Some(true); + self.user_agent_original = Some(StandardAttribute::Bool(true)); } } TelemetryDataKind::Metrics => {} @@ -588,20 +604,23 @@ impl Selectors for RouterAttributes { fn on_request(&self, request: &router::Request) -> Vec { let mut attrs = self.common.on_request(request); attrs.extend(self.server.on_request(request)); - if let Some(true) = &self.trace_id { + if let Some(key) = self + .trace_id + .as_ref() + .and_then(|a| a.key(Key::from_static_str("trace_id"))) + { if let Some(trace_id) = trace_id() { - attrs.push(KeyValue::new( - Key::from_static_str("trace_id"), - trace_id.to_string(), - )); + attrs.push(KeyValue::new(key, trace_id.to_string())); } } - if let Some(true) = &self.datadog_trace_id { + + if let Some(key) = self + .datadog_trace_id + .as_ref() + .and_then(|a| a.key(Key::from_static_str("dd.trace_id"))) + { if let Some(trace_id) = trace_id() { - attrs.push(KeyValue::new( - Key::from_static_str("dd.trace_id"), - trace_id.to_datadog(), - )); + attrs.push(KeyValue::new(key, trace_id.to_datadog())); } } if let Some(true) = &self.baggage { @@ -635,14 +654,22 @@ impl Selectors for HttpCommonAttributes { fn on_request(&self, request: &router::Request) -> Vec { let mut attrs = Vec::new(); - if let Some(true) = &self.http_request_method { + if let Some(key) = self + .http_request_method + .as_ref() + .and_then(|a| a.key(HTTP_REQUEST_METHOD)) + { attrs.push(KeyValue::new( - HTTP_REQUEST_METHOD, + key, request.router_request.method().as_str().to_string(), )); } - if let Some(true) = &self.http_request_body_size { + if let Some(key) = self + .http_request_body_size + .as_ref() + .and_then(|a| a.key(HTTP_REQUEST_BODY_SIZE)) + { if let Some(content_length) = request .router_request .headers() @@ -651,35 +678,47 @@ impl Selectors for HttpCommonAttributes { { if let Ok(content_length) = content_length.parse::() { attrs.push(KeyValue::new( - HTTP_REQUEST_BODY_SIZE, + key, opentelemetry::Value::I64(content_length), )); } } } - if let Some(true) = &self.network_protocol_name { + if let Some(key) = self + .network_protocol_name + .as_ref() + .and_then(|a| a.key(NETWORK_PROTOCOL_NAME)) + { if let Some(scheme) = request.router_request.uri().scheme() { - attrs.push(KeyValue::new(NETWORK_PROTOCOL_NAME, scheme.to_string())); + attrs.push(KeyValue::new(key, scheme.to_string())); } } - if let Some(true) = &self.network_protocol_version { + if let Some(key) = self + .network_protocol_version + .as_ref() + .and_then(|a| a.key(NETWORK_PROTOCOL_VERSION)) + { attrs.push(KeyValue::new( - NETWORK_PROTOCOL_VERSION, + key, format!("{:?}", request.router_request.version()), )); } - if let Some(true) = &self.network_transport { - attrs.push(KeyValue::new(NETWORK_TRANSPORT, "tcp".to_string())); + if let Some(key) = self + .network_transport + .as_ref() + .and_then(|a| a.key(NETWORK_TRANSPORT)) + { + attrs.push(KeyValue::new(key, "tcp".to_string())); } - if let Some(true) = &self.network_type { + if let Some(key) = self.network_type.as_ref().and_then(|a| a.key(NETWORK_TYPE)) { if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.server_address { if socket.is_ipv4() { - attrs.push(KeyValue::new(NETWORK_TYPE, "ipv4".to_string())); + attrs.push(KeyValue::new(key, "ipv4".to_string())); } else if socket.is_ipv6() { - attrs.push(KeyValue::new(NETWORK_TYPE, "ipv6".to_string())); + attrs.push(KeyValue::new(key, "ipv6".to_string())); } } } @@ -690,7 +729,11 @@ impl Selectors for HttpCommonAttributes { fn on_response(&self, response: &router::Response) -> Vec { let mut attrs = Vec::new(); - if let Some(true) = &self.http_response_body_size { + if let Some(key) = self + .http_response_body_size + .as_ref() + .and_then(|a| a.key(HTTP_RESPONSE_BODY_SIZE)) + { if let Some(content_length) = response .response .headers() @@ -699,24 +742,28 @@ impl Selectors for HttpCommonAttributes { { if let Ok(content_length) = content_length.parse::() { attrs.push(KeyValue::new( - HTTP_RESPONSE_BODY_SIZE, + key, opentelemetry::Value::I64(content_length), )); } } } - if let Some(true) = &self.http_response_status_code { + if let Some(key) = self + .http_response_status_code + .as_ref() + .and_then(|a| a.key(HTTP_RESPONSE_STATUS_CODE)) + { attrs.push(KeyValue::new( - HTTP_RESPONSE_STATUS_CODE, + key, response.response.status().as_u16() as i64, )); } - if let Some(true) = &self.error_type { + if let Some(key) = self.error_type.as_ref().and_then(|a| a.key(ERROR_TYPE)) { if !response.response.status().is_success() { attrs.push(KeyValue::new( - ERROR_TYPE, + key, response .response .status() @@ -731,17 +778,21 @@ impl Selectors for HttpCommonAttributes { fn on_error(&self, _error: &BoxError, _ctx: &Context) -> Vec { let mut attrs = Vec::new(); - if let Some(true) = &self.error_type { + if let Some(key) = self.error_type.as_ref().and_then(|a| a.key(ERROR_TYPE)) { attrs.push(KeyValue::new( - ERROR_TYPE, + key, StatusCode::INTERNAL_SERVER_ERROR .canonical_reason() .unwrap_or("unknown"), )); } - if let Some(true) = &self.http_response_status_code { + if let Some(key) = self + .http_response_status_code + .as_ref() + .and_then(|a| a.key(HTTP_RESPONSE_STATUS_CODE)) + { attrs.push(KeyValue::new( - HTTP_RESPONSE_STATUS_CODE, + key, StatusCode::INTERNAL_SERVER_ERROR.as_u16() as i64, )); } @@ -757,123 +808,148 @@ impl Selectors for HttpServerAttributes { fn on_request(&self, request: &router::Request) -> Vec { let mut attrs = Vec::new(); - if let Some(true) = &self.http_route { + if let Some(key) = self.http_route.as_ref().and_then(|a| a.key(HTTP_ROUTE)) { attrs.push(KeyValue::new( - HTTP_ROUTE, + key, request.router_request.uri().path().to_string(), )); } - if let Some(true) = &self.client_address { + if let Some(key) = self + .client_address + .as_ref() + .and_then(|a| a.key(CLIENT_ADDRESS)) + { if let Some(forwarded) = Self::forwarded_for(request) { - attrs.push(KeyValue::new(CLIENT_ADDRESS, forwarded.ip().to_string())); + attrs.push(KeyValue::new(key, forwarded.ip().to_string())); } else if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.peer_address { - attrs.push(KeyValue::new(CLIENT_ADDRESS, socket.ip().to_string())); + attrs.push(KeyValue::new(key, socket.ip().to_string())); } } } - if let Some(true) = &self.client_port { + if let Some(key) = self.client_port.as_ref().and_then(|a| a.key(CLIENT_PORT)) { if let Some(forwarded) = Self::forwarded_for(request) { - attrs.push(KeyValue::new(CLIENT_PORT, forwarded.port() as i64)); + attrs.push(KeyValue::new(key, forwarded.port() as i64)); } else if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.peer_address { - attrs.push(KeyValue::new(CLIENT_PORT, socket.port() as i64)); + attrs.push(KeyValue::new(key, socket.port() as i64)); } } } - if let Some(true) = &self.server_address { + if let Some(key) = self + .server_address + .as_ref() + .and_then(|a| a.key(SERVER_ADDRESS)) + { if let Some(forwarded) = Self::forwarded_host(request).and_then(|h| h.host().map(|h| h.to_string())) { - attrs.push(KeyValue::new(SERVER_ADDRESS, forwarded)); + attrs.push(KeyValue::new(key, forwarded)); } else if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.server_address { - attrs.push(KeyValue::new(SERVER_ADDRESS, socket.ip().to_string())); + attrs.push(KeyValue::new(key, socket.ip().to_string())); } } } - if let Some(true) = &self.server_port { + if let Some(key) = self.server_port.as_ref().and_then(|a| a.key(SERVER_PORT)) { if let Some(forwarded) = Self::forwarded_host(request).and_then(|h| h.port_u16()) { - attrs.push(KeyValue::new(SERVER_PORT, forwarded as i64)); + attrs.push(KeyValue::new(key, forwarded as i64)); } else if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.server_address { - attrs.push(KeyValue::new(SERVER_PORT, socket.port() as i64)); + attrs.push(KeyValue::new(key, socket.port() as i64)); } } } - if let Some(true) = &self.network_local_address { + if let Some(key) = self + .network_local_address + .as_ref() + .and_then(|a| a.key(NETWORK_LOCAL_ADDRESS)) + { if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.server_address { - attrs.push(KeyValue::new( - NETWORK_LOCAL_ADDRESS, - socket.ip().to_string(), - )); + attrs.push(KeyValue::new(key, socket.ip().to_string())); } } } - if let Some(true) = &self.network_local_port { + if let Some(key) = self + .network_local_port + .as_ref() + .and_then(|a| a.key(NETWORK_LOCAL_PORT)) + { if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.server_address { - attrs.push(KeyValue::new(NETWORK_LOCAL_PORT, socket.port() as i64)); + attrs.push(KeyValue::new(key, socket.port() as i64)); } } } - if let Some(true) = &self.network_peer_address { + if let Some(key) = self + .network_peer_address + .as_ref() + .and_then(|a| a.key(NETWORK_PEER_ADDRESS)) + { if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.peer_address { - attrs.push(KeyValue::new(NETWORK_PEER_ADDRESS, socket.ip().to_string())); + attrs.push(KeyValue::new(key, socket.ip().to_string())); } } } - if let Some(true) = &self.network_peer_port { + if let Some(key) = self + .network_peer_port + .as_ref() + .and_then(|a| a.key(NETWORK_PEER_PORT)) + { if let Some(connection_info) = request.router_request.extensions().get::() { if let Some(socket) = connection_info.peer_address { - attrs.push(KeyValue::new(NETWORK_PEER_PORT, socket.port() as i64)); + attrs.push(KeyValue::new(key, socket.port() as i64)); } } } let router_uri = request.router_request.uri(); - if let Some(true) = &self.url_path { - attrs.push(KeyValue::new(URL_PATH, router_uri.path().to_string())); + if let Some(key) = self.url_path.as_ref().and_then(|a| a.key(URL_PATH)) { + attrs.push(KeyValue::new(key, router_uri.path().to_string())); } - if let Some(true) = &self.url_query { + if let Some(key) = self.url_query.as_ref().and_then(|a| a.key(URL_QUERY)) { if let Some(query) = router_uri.query() { - attrs.push(KeyValue::new(URL_QUERY, query.to_string())); + attrs.push(KeyValue::new(key, query.to_string())); } } - if let Some(true) = &self.url_scheme { + if let Some(key) = self.url_scheme.as_ref().and_then(|a| a.key(URL_SCHEME)) { if let Some(scheme) = router_uri.scheme_str() { - attrs.push(KeyValue::new(URL_SCHEME, scheme.to_string())); + attrs.push(KeyValue::new(key, scheme.to_string())); } } - if let Some(true) = &self.user_agent_original { + if let Some(key) = self + .user_agent_original + .as_ref() + .and_then(|a| a.key(USER_AGENT_ORIGINAL)) + { if let Some(user_agent) = request .router_request .headers() .get(&USER_AGENT) .and_then(|h| h.to_str().ok()) { - attrs.push(KeyValue::new(USER_AGENT_ORIGINAL, user_agent.to_string())); + attrs.push(KeyValue::new(key, user_agent.to_string())); } } @@ -934,33 +1010,39 @@ impl Selectors for SupergraphAttributes { fn on_request(&self, request: &supergraph::Request) -> Vec { let mut attrs = Vec::new(); - if let Some(true) = &self.graphql_document { + if let Some(key) = self + .graphql_document + .as_ref() + .and_then(|a| a.key(GRAPHQL_DOCUMENT)) + { if let Some(query) = &request.supergraph_request.body().query { - attrs.push(KeyValue::new(GRAPHQL_DOCUMENT, query.clone())); + attrs.push(KeyValue::new(key, query.clone())); } } - if let Some(true) = &self.graphql_operation_name { + if let Some(key) = self + .graphql_operation_name + .as_ref() + .and_then(|a| a.key(GRAPHQL_OPERATION_NAME)) + { if let Some(operation_name) = &request .context .get::<_, String>(OPERATION_NAME) .unwrap_or_default() { - attrs.push(KeyValue::new( - GRAPHQL_OPERATION_NAME, - operation_name.clone(), - )); + attrs.push(KeyValue::new(key, operation_name.clone())); } } - if let Some(true) = &self.graphql_operation_type { + if let Some(key) = self + .graphql_operation_type + .as_ref() + .and_then(|a| a.key(GRAPHQL_OPERATION_TYPE)) + { if let Some(operation_type) = &request .context .get::<_, String>(OPERATION_KIND) .unwrap_or_default() { - attrs.push(KeyValue::new( - GRAPHQL_OPERATION_TYPE, - operation_type.clone(), - )); + attrs.push(KeyValue::new(key, operation_type.clone())); } } @@ -995,35 +1077,45 @@ impl Selectors for SubgraphAttributes { fn on_request(&self, request: &subgraph::Request) -> Vec { let mut attrs = Vec::new(); - if let Some(true) = &self.graphql_document { + if let Some(key) = self + .graphql_document + .as_ref() + .and_then(|a| a.key(SUBGRAPH_GRAPHQL_DOCUMENT)) + { if let Some(query) = &request.subgraph_request.body().query { - attrs.push(KeyValue::new(SUBGRAPH_GRAPHQL_DOCUMENT, query.clone())); + attrs.push(KeyValue::new(key, query.clone())); } } - if let Some(true) = &self.graphql_operation_name { + if let Some(key) = self + .graphql_operation_name + .as_ref() + .and_then(|a| a.key(SUBGRAPH_GRAPHQL_OPERATION_NAME)) + { if let Some(op_name) = &request.subgraph_request.body().operation_name { - attrs.push(KeyValue::new( - SUBGRAPH_GRAPHQL_OPERATION_NAME, - op_name.clone(), - )); + attrs.push(KeyValue::new(key, op_name.clone())); } } - if let Some(true) = &self.graphql_operation_type { + if let Some(key) = self + .graphql_operation_type + .as_ref() + .and_then(|a| a.key(SUBGRAPH_GRAPHQL_OPERATION_TYPE)) + { // Subgraph operation type wil always match the supergraph operation type if let Some(operation_type) = &request .context .get::<_, String>(OPERATION_KIND) .unwrap_or_default() { - attrs.push(KeyValue::new( - SUBGRAPH_GRAPHQL_OPERATION_TYPE, - operation_type.clone(), - )); + attrs.push(KeyValue::new(key, operation_type.clone())); } } - if let Some(true) = &self.subgraph_name { + if let Some(key) = self + .subgraph_name + .as_ref() + .and_then(|a| a.key(SUBGRAPH_NAME)) + { if let Some(subgraph_name) = &request.subgraph_name { - attrs.push(KeyValue::new(SUBGRAPH_NAME, subgraph_name.clone())); + attrs.push(KeyValue::new(key, subgraph_name.clone())); } } @@ -1090,6 +1182,7 @@ mod test { use crate::plugins::telemetry::config_new::attributes::HttpCommonAttributes; use crate::plugins::telemetry::config_new::attributes::HttpServerAttributes; use crate::plugins::telemetry::config_new::attributes::RouterAttributes; + use crate::plugins::telemetry::config_new::attributes::StandardAttribute; use crate::plugins::telemetry::config_new::attributes::SubgraphAttributes; use crate::plugins::telemetry::config_new::attributes::SupergraphAttributes; use crate::plugins::telemetry::config_new::attributes::ERROR_TYPE; @@ -1129,8 +1222,8 @@ mod test { let _guard = span.enter(); let attributes = RouterAttributes { - datadog_trace_id: Some(true), - trace_id: Some(true), + datadog_trace_id: Some(StandardAttribute::Bool(true)), + trace_id: Some(StandardAttribute::Bool(true)), baggage: Some(true), common: Default::default(), server: Default::default(), @@ -1171,13 +1264,45 @@ mod test { .map(|key_val| &key_val.value), Some(&"baggage_value_bis".into()) ); + + let attributes = RouterAttributes { + datadog_trace_id: Some(StandardAttribute::Aliased { + alias: "datatoutou_id".to_string(), + }), + trace_id: Some(StandardAttribute::Aliased { + alias: "my_trace_id".to_string(), + }), + baggage: Some(false), + common: Default::default(), + server: Default::default(), + }; + let attributes = + attributes.on_request(&router::Request::fake_builder().build().unwrap()); + + assert_eq!( + attributes + .iter() + .find( + |key_val| key_val.key == opentelemetry::Key::from_static_str("my_trace_id") + ) + .map(|key_val| &key_val.value), + Some(&"0000000000000000000000000000002a".into()) + ); + assert_eq!( + attributes + .iter() + .find(|key_val| key_val.key + == opentelemetry::Key::from_static_str("datatoutou_id")) + .map(|key_val| &key_val.value), + Some(&"42".into()) + ); }); } #[test] fn test_supergraph_graphql_document() { let attributes = SupergraphAttributes { - graphql_document: Some(true), + graphql_document: Some(StandardAttribute::Bool(true)), ..Default::default() }; let attributes = attributes.on_request( @@ -1198,7 +1323,7 @@ mod test { #[test] fn test_supergraph_graphql_operation_name() { let attributes = SupergraphAttributes { - graphql_operation_name: Some(true), + graphql_operation_name: Some(StandardAttribute::Bool(true)), ..Default::default() }; let context = crate::Context::new(); @@ -1216,12 +1341,33 @@ mod test { .map(|key_val| &key_val.value), Some(&"topProducts".into()) ); + let attributes = SupergraphAttributes { + graphql_operation_name: Some(StandardAttribute::Aliased { + alias: String::from("graphql_query"), + }), + ..Default::default() + }; + let context = crate::Context::new(); + let _ = context.insert(OPERATION_NAME, "topProducts".to_string()); + let attributes = attributes.on_request( + &supergraph::Request::fake_builder() + .context(context) + .build() + .unwrap(), + ); + assert_eq!( + attributes + .iter() + .find(|key_val| key_val.key.as_str() == "graphql_query") + .map(|key_val| &key_val.value), + Some(&"topProducts".into()) + ); } #[test] fn test_supergraph_graphql_operation_type() { let attributes = SupergraphAttributes { - graphql_operation_type: Some(true), + graphql_operation_type: Some(StandardAttribute::Bool(true)), ..Default::default() }; let context = crate::Context::new(); @@ -1244,7 +1390,7 @@ mod test { #[test] fn test_subgraph_graphql_document() { let attributes = SubgraphAttributes { - graphql_document: Some(true), + graphql_document: Some(StandardAttribute::Bool(true)), ..Default::default() }; let attributes = attributes.on_request( @@ -1273,7 +1419,7 @@ mod test { #[test] fn test_subgraph_graphql_operation_name() { let attributes = SubgraphAttributes { - graphql_operation_name: Some(true), + graphql_operation_name: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1303,7 +1449,7 @@ mod test { #[test] fn test_subgraph_graphql_operation_type() { let attributes = SubgraphAttributes { - graphql_operation_type: Some(true), + graphql_operation_type: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1332,7 +1478,7 @@ mod test { #[test] fn test_subgraph_name() { let attributes = SubgraphAttributes { - subgraph_name: Some(true), + subgraph_name: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1359,7 +1505,7 @@ mod test { #[test] fn test_http_common_error_type() { let common = HttpCommonAttributes { - error_type: Some(true), + error_type: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1400,7 +1546,7 @@ mod test { #[test] fn test_http_common_request_body_size() { let common = HttpCommonAttributes { - http_request_body_size: Some(true), + http_request_body_size: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1425,7 +1571,7 @@ mod test { #[test] fn test_http_common_response_body_size() { let common = HttpCommonAttributes { - http_response_body_size: Some(true), + http_response_body_size: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1450,7 +1596,7 @@ mod test { #[test] fn test_http_common_request_method() { let common = HttpCommonAttributes { - http_request_method: Some(true), + http_request_method: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1472,7 +1618,7 @@ mod test { #[test] fn test_http_common_response_status_code() { let common = HttpCommonAttributes { - http_response_status_code: Some(true), + http_response_status_code: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1503,7 +1649,7 @@ mod test { #[test] fn test_http_common_network_protocol_name() { let common = HttpCommonAttributes { - network_protocol_name: Some(true), + network_protocol_name: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1525,7 +1671,7 @@ mod test { #[test] fn test_http_common_network_protocol_version() { let common = HttpCommonAttributes { - network_protocol_version: Some(true), + network_protocol_version: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1547,7 +1693,7 @@ mod test { #[test] fn test_http_common_network_transport() { let common = HttpCommonAttributes { - network_transport: Some(true), + network_transport: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1564,7 +1710,7 @@ mod test { #[test] fn test_http_common_network_type() { let common = HttpCommonAttributes { - network_type: Some(true), + network_type: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1586,7 +1732,7 @@ mod test { #[test] fn test_http_server_client_address() { let server = HttpServerAttributes { - client_address: Some(true), + client_address: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1625,7 +1771,7 @@ mod test { #[test] fn test_http_server_client_port() { let server = HttpServerAttributes { - client_port: Some(true), + client_port: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1664,7 +1810,7 @@ mod test { #[test] fn test_http_server_http_route() { let server = HttpServerAttributes { - http_route: Some(true), + http_route: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1689,7 +1835,7 @@ mod test { #[test] fn test_http_server_network_local_address() { let server = HttpServerAttributes { - network_local_address: Some(true), + network_local_address: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1714,7 +1860,7 @@ mod test { #[test] fn test_http_server_network_local_port() { let server = HttpServerAttributes { - network_local_port: Some(true), + network_local_port: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1739,7 +1885,7 @@ mod test { #[test] fn test_http_server_network_peer_address() { let server = HttpServerAttributes { - network_peer_address: Some(true), + network_peer_address: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1764,7 +1910,7 @@ mod test { #[test] fn test_http_server_network_peer_port() { let server = HttpServerAttributes { - network_peer_port: Some(true), + network_peer_port: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1789,7 +1935,7 @@ mod test { #[test] fn test_http_server_server_address() { let server = HttpServerAttributes { - server_address: Some(true), + server_address: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1828,7 +1974,7 @@ mod test { #[test] fn test_http_server_server_port() { let server = HttpServerAttributes { - server_port: Some(true), + server_port: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1866,7 +2012,7 @@ mod test { #[test] fn test_http_server_url_path() { let server = HttpServerAttributes { - url_path: Some(true), + url_path: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1887,7 +2033,7 @@ mod test { #[test] fn test_http_server_query() { let server = HttpServerAttributes { - url_query: Some(true), + url_query: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1908,7 +2054,7 @@ mod test { #[test] fn test_http_server_scheme() { let server = HttpServerAttributes { - url_scheme: Some(true), + url_scheme: Some(StandardAttribute::Bool(true)), ..Default::default() }; @@ -1930,7 +2076,7 @@ mod test { #[test] fn test_http_server_user_agent_original() { let server = HttpServerAttributes { - user_agent_original: Some(true), + user_agent_original: Some(StandardAttribute::Bool(true)), ..Default::default() }; diff --git a/apollo-router/src/plugins/telemetry/config_new/cache/attributes.rs b/apollo-router/src/plugins/telemetry/config_new/cache/attributes.rs index 9d072cfa3c..00d0b4b240 100644 --- a/apollo-router/src/plugins/telemetry/config_new/cache/attributes.rs +++ b/apollo-router/src/plugins/telemetry/config_new/cache/attributes.rs @@ -3,6 +3,7 @@ use schemars::JsonSchema; use serde::Deserialize; use tower::BoxError; +use crate::plugins::telemetry::config_new::attributes::StandardAttribute; use crate::plugins::telemetry::config_new::DefaultAttributeRequirementLevel; use crate::plugins::telemetry::config_new::DefaultForLevel; use crate::plugins::telemetry::config_new::Selectors; @@ -14,8 +15,8 @@ use crate::Context; #[serde(deny_unknown_fields, default)] pub(crate) struct CacheAttributes { /// Entity type - #[serde(rename = "entity.type")] - pub(crate) entity_type: Option, + #[serde(rename = "graphql.type.name")] + pub(crate) entity_type: Option, } impl DefaultForLevel for CacheAttributes { @@ -26,7 +27,8 @@ impl DefaultForLevel for CacheAttributes { ) { if let TelemetryDataKind::Metrics = kind { if let DefaultAttributeRequirementLevel::Required = requirement_level { - self.entity_type.get_or_insert(false); + self.entity_type + .get_or_insert(StandardAttribute::Bool(false)); } } } diff --git a/apollo-router/src/plugins/telemetry/config_new/cache/mod.rs b/apollo-router/src/plugins/telemetry/config_new/cache/mod.rs index adc172911b..7000278edc 100644 --- a/apollo-router/src/plugins/telemetry/config_new/cache/mod.rs +++ b/apollo-router/src/plugins/telemetry/config_new/cache/mod.rs @@ -24,7 +24,7 @@ use crate::services::subgraph; pub(crate) mod attributes; pub(crate) const CACHE_METRIC: &str = "apollo.router.operations.entity.cache"; -const ENTITY_TYPE: Key = Key::from_static_str("entity.type"); +const ENTITY_TYPE: Key = Key::from_static_str("graphql.type.name"); const CACHE_HIT: Key = Key::from_static_str("cache.hit"); #[derive(Deserialize, JsonSchema, Clone, Default, Debug)] @@ -93,14 +93,14 @@ impl Instrumented for CacheInstruments { inner_cache_hit.selector = Some(Arc::new(SubgraphSelector::StaticField { r#static: AttributeValue::I64(*hit as i64), })); - if inner_cache_hit + if let Some(key) = inner_cache_hit .selectors .as_ref() - .map(|s| s.attributes.entity_type == Some(true)) - .unwrap_or_default() + .and_then(|s| s.attributes.entity_type.as_ref()) + .and_then(|a| a.key(ENTITY_TYPE)) { inner_cache_hit.attributes.push(KeyValue::new( - ENTITY_TYPE, + key, opentelemetry::Value::String(entity_type.to_string().into()), )); } @@ -118,14 +118,14 @@ impl Instrumented for CacheInstruments { inner_cache_miss.selector = Some(Arc::new(SubgraphSelector::StaticField { r#static: AttributeValue::I64(*miss as i64), })); - if inner_cache_miss + if let Some(key) = inner_cache_miss .selectors .as_ref() - .map(|s| s.attributes.entity_type == Some(true)) - .unwrap_or_default() + .and_then(|s| s.attributes.entity_type.as_ref()) + .and_then(|a| a.key(ENTITY_TYPE)) { inner_cache_miss.attributes.push(KeyValue::new( - ENTITY_TYPE, + key, opentelemetry::Value::String(entity_type.to_string().into()), )); } diff --git a/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs b/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs index 503b191904..8341790eac 100644 --- a/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs +++ b/apollo-router/src/plugins/telemetry/config_new/cost/mod.rs @@ -9,10 +9,14 @@ use schemars::JsonSchema; use serde::Deserialize; use tower::BoxError; +use super::attributes::StandardAttribute; use super::instruments::Increment; use super::instruments::StaticInstrument; use crate::metrics; -use crate::plugins::demand_control::CostContext; +use crate::plugins::demand_control::COST_ACTUAL_KEY; +use crate::plugins::demand_control::COST_DELTA_KEY; +use crate::plugins::demand_control::COST_ESTIMATED_KEY; +use crate::plugins::demand_control::COST_RESULT_KEY; use crate::plugins::telemetry::config::AttributeValue; use crate::plugins::telemetry::config_new::attributes::SupergraphAttributes; use crate::plugins::telemetry::config_new::conditions::Condition; @@ -37,26 +41,22 @@ pub(crate) const APOLLO_PRIVATE_COST_STRATEGY: Key = pub(crate) const APOLLO_PRIVATE_COST_RESULT: Key = Key::from_static_str("apollo_private.cost.result"); -static COST_ESTIMATED: &str = "cost.estimated"; -static COST_ACTUAL: &str = "cost.actual"; -static COST_DELTA: &str = "cost.delta"; - /// Attributes for Cost #[derive(Deserialize, JsonSchema, Clone, Default, Debug, PartialEq)] #[serde(deny_unknown_fields, default)] pub(crate) struct SupergraphCostAttributes { /// The estimated cost of the operation using the currently configured cost model #[serde(rename = "cost.estimated")] - cost_estimated: Option, + cost_estimated: Option, /// The actual cost of the operation using the currently configured cost model #[serde(rename = "cost.actual")] - cost_actual: Option, + cost_actual: Option, /// The delta (estimated - actual) cost of the operation using the currently configured cost model #[serde(rename = "cost.delta")] - cost_delta: Option, + cost_delta: Option, /// The cost result, this is an error code returned by the cost calculation or COST_OK #[serde(rename = "cost.result")] - cost_result: Option, + cost_result: Option, } impl Selectors for SupergraphCostAttributes { @@ -78,27 +78,60 @@ impl Selectors for SupergraphCostAttributes { fn on_response_event(&self, _response: &Self::EventResponse, ctx: &Context) -> Vec { let mut attrs = Vec::with_capacity(4); - let cost_result = ctx - .extensions() - .with_lock(|lock| lock.get::().cloned()); - if let Some(cost_result) = cost_result { - if let Some(true) = self.cost_estimated { - attrs.push(KeyValue::new("cost.estimated", cost_result.estimated)); - } - if let Some(true) = self.cost_actual { - attrs.push(KeyValue::new("cost.actual", cost_result.actual)); - } - if let Some(true) = self.cost_delta { - attrs.push(KeyValue::new("cost.delta", cost_result.delta())); - } - if let Some(true) = self.cost_result { - attrs.push(KeyValue::new("cost.result", cost_result.result)); - } + if let Some(estimated_cost) = self.estimated_cost_if_configured(ctx) { + attrs.push(estimated_cost); + } + if let Some(actual_cost) = self.actual_cost_if_configured(ctx) { + attrs.push(actual_cost); + } + if let Some(cost_delta) = self.cost_delta_if_configured(ctx) { + attrs.push(cost_delta); + } + if let Some(cost_result) = self.cost_result_if_configured(ctx) { + attrs.push(cost_result); } attrs } } +impl SupergraphCostAttributes { + fn estimated_cost_if_configured(&self, ctx: &Context) -> Option { + let key = self + .cost_estimated + .as_ref()? + .key(Key::from_static_str(COST_ESTIMATED_KEY))?; + let value = ctx.get_estimated_cost().ok()??; + Some(KeyValue::new(key, value)) + } + + fn actual_cost_if_configured(&self, ctx: &Context) -> Option { + let key = self + .cost_actual + .as_ref()? + .key(Key::from_static_str(COST_ACTUAL_KEY))?; + let value = ctx.get_actual_cost().ok()??; + Some(KeyValue::new(key, value)) + } + + fn cost_delta_if_configured(&self, ctx: &Context) -> Option { + let key = self + .cost_delta + .as_ref()? + .key(Key::from_static_str("cost.delta"))?; + let value = ctx.get_cost_delta().ok()??; + Some(KeyValue::new(key, value)) + } + + fn cost_result_if_configured(&self, ctx: &Context) -> Option { + let key = self + .cost_result + .as_ref()? + .key(Key::from_static_str(COST_RESULT_KEY))?; + let value = ctx.get_cost_result().ok()??; + Some(KeyValue::new(key, value)) + } +} + #[derive(Deserialize, JsonSchema, Clone, Default, Debug)] #[serde(deny_unknown_fields, default)] pub(crate) struct CostInstrumentsConfig { @@ -122,14 +155,14 @@ impl CostInstrumentsConfig { .meter(crate::plugins::telemetry::config_new::instruments::METER_NAME); [( - COST_ESTIMATED.to_string(), - StaticInstrument::Histogram(meter.f64_histogram(COST_ESTIMATED).with_description("Estimated cost of the operation using the currently configured cost model").init()), + COST_ESTIMATED_KEY.to_string(), + StaticInstrument::Histogram(meter.f64_histogram(COST_ESTIMATED_KEY).with_description("Estimated cost of the operation using the currently configured cost model").init()), ),( - COST_ACTUAL.to_string(), - StaticInstrument::Histogram(meter.f64_histogram(COST_ACTUAL).with_description("Actual cost of the operation using the currently configured cost model").init()), + COST_ACTUAL_KEY.to_string(), + StaticInstrument::Histogram(meter.f64_histogram(COST_ACTUAL_KEY).with_description("Actual cost of the operation using the currently configured cost model").init()), ),( - COST_DELTA.to_string(), - StaticInstrument::Histogram(meter.f64_histogram(COST_DELTA).with_description("Delta between the estimated and actual cost of the operation using the currently configured cost model").init()), + COST_DELTA_KEY.to_string(), + StaticInstrument::Histogram(meter.f64_histogram(COST_DELTA_KEY).with_description("Delta between the estimated and actual cost of the operation using the currently configured cost model").init()), )] .into_iter() .collect() @@ -141,7 +174,7 @@ impl CostInstrumentsConfig { ) -> CostInstruments { let cost_estimated = self.cost_estimated.is_enabled().then(|| { Self::histogram( - COST_ESTIMATED, + COST_ESTIMATED_KEY, &self.cost_estimated, SupergraphSelector::Cost { cost: CostValue::Estimated, @@ -152,7 +185,7 @@ impl CostInstrumentsConfig { let cost_actual = self.cost_actual.is_enabled().then(|| { Self::histogram( - COST_ACTUAL, + COST_ACTUAL_KEY, &self.cost_actual, SupergraphSelector::Cost { cost: CostValue::Actual, @@ -163,7 +196,7 @@ impl CostInstrumentsConfig { let cost_delta = self.cost_delta.is_enabled().then(|| { Self::histogram( - COST_DELTA, + COST_DELTA_KEY, &self.cost_delta, SupergraphSelector::Cost { cost: CostValue::Delta, @@ -314,26 +347,30 @@ pub(crate) enum CostValue { } pub(crate) fn add_cost_attributes(context: &Context, custom_attributes: &mut Vec) { - context.extensions().with_lock(|c| { - if let Some(cost) = c.get::() { - custom_attributes.push(KeyValue::new( - APOLLO_PRIVATE_COST_ESTIMATED.clone(), - AttributeValue::F64(cost.estimated), - )); - custom_attributes.push(KeyValue::new( - APOLLO_PRIVATE_COST_ACTUAL.clone(), - AttributeValue::F64(cost.actual), - )); - custom_attributes.push(KeyValue::new( - APOLLO_PRIVATE_COST_RESULT.clone(), - AttributeValue::String(cost.result.into()), - )); - custom_attributes.push(KeyValue::new( - APOLLO_PRIVATE_COST_STRATEGY.clone(), - AttributeValue::String(cost.strategy.into()), - )); - } - }); + if let Ok(Some(cost)) = context.get_estimated_cost() { + custom_attributes.push(KeyValue::new( + APOLLO_PRIVATE_COST_ESTIMATED.clone(), + AttributeValue::F64(cost), + )); + } + if let Ok(Some(cost)) = context.get_actual_cost() { + custom_attributes.push(KeyValue::new( + APOLLO_PRIVATE_COST_ACTUAL.clone(), + AttributeValue::F64(cost), + )); + } + if let Ok(Some(result)) = context.get_cost_result() { + custom_attributes.push(KeyValue::new( + APOLLO_PRIVATE_COST_RESULT.clone(), + AttributeValue::String(result), + )); + } + if let Ok(Some(strategy)) = context.get_cost_strategy() { + custom_attributes.push(KeyValue::new( + APOLLO_PRIVATE_COST_STRATEGY.clone(), + AttributeValue::String(strategy), + )); + } } #[cfg(test)] @@ -341,7 +378,6 @@ mod test { use std::sync::Arc; use crate::context::OPERATION_NAME; - use crate::plugins::demand_control::CostContext; use crate::plugins::telemetry::config_new::cost::CostInstruments; use crate::plugins::telemetry::config_new::cost::CostInstrumentsConfig; use crate::plugins::telemetry::config_new::instruments::Instrumented; @@ -446,13 +482,11 @@ mod test { fn make_request(instruments: &CostInstruments) { let context = Context::new(); - context.extensions().with_lock(|mut lock| { - lock.insert(CostContext::default()); - let cost_result = lock.get_or_default_mut::(); - cost_result.estimated = 100.0; - cost_result.actual = 10.0; - cost_result.result = "COST_TOO_EXPENSIVE" - }); + context.insert_estimated_cost(100.0).unwrap(); + context.insert_actual_cost(10.0).unwrap(); + context + .insert_cost_result("COST_TOO_EXPENSIVE".to_string()) + .unwrap(); let _ = context.insert(OPERATION_NAME, "Test".to_string()).unwrap(); instruments.on_request( &supergraph::Request::fake_builder() diff --git a/apollo-router/src/plugins/telemetry/config_new/extendable.rs b/apollo-router/src/plugins/telemetry/config_new/extendable.rs index 6af5d2bf1c..c515a352bd 100644 --- a/apollo-router/src/plugins/telemetry/config_new/extendable.rs +++ b/apollo-router/src/plugins/telemetry/config_new/extendable.rs @@ -283,6 +283,7 @@ mod test { use crate::plugins::telemetry::config_new::attributes::HttpCommonAttributes; use crate::plugins::telemetry::config_new::attributes::HttpServerAttributes; use crate::plugins::telemetry::config_new::attributes::RouterAttributes; + use crate::plugins::telemetry::config_new::attributes::StandardAttribute; use crate::plugins::telemetry::config_new::attributes::SupergraphAttributes; use crate::plugins::telemetry::config_new::conditional::Conditional; use crate::plugins::telemetry::config_new::conditions::Condition; @@ -312,8 +313,8 @@ mod test { extendable_conf.attributes, SupergraphAttributes { graphql_document: None, - graphql_operation_name: Some(true), - graphql_operation_type: Some(true), + graphql_operation_name: Some(StandardAttribute::Bool(true)), + graphql_operation_type: Some(StandardAttribute::Bool(true)), cost: Default::default() } ); @@ -384,12 +385,12 @@ mod test { trace_id: None, baggage: None, common: HttpCommonAttributes { - http_request_method: Some(true), - http_response_status_code: Some(true), + http_request_method: Some(StandardAttribute::Bool(true)), + http_response_status_code: Some(StandardAttribute::Bool(true)), ..Default::default() }, server: HttpServerAttributes { - url_path: Some(true), + url_path: Some(StandardAttribute::Bool(true)), ..Default::default() } } diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/metrics.snap index 5d112315d7..c586792480 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/metrics.snap +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/metrics.snap @@ -10,6 +10,8 @@ info: http.server.active_requests: false http.server.request.duration: attributes: + http.request.method: + alias: http_method my_attribute: request_method: true graphql.operation.name: @@ -23,6 +25,6 @@ info: - sum: 0.1 attributes: graphql.operation.name: TestQuery - http.request.method: GET http.response.status_code: 200 + http_method: GET my_attribute: GET diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/router.yaml index 7efc06671c..ef8332ead1 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/router.yaml +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/router/http.server.request.duration_with_custom_attributes/router.yaml @@ -5,6 +5,8 @@ telemetry: http.server.active_requests: false http.server.request.duration: attributes: + http.request.method: + alias: http_method my_attribute: request_method: true graphql.operation.name: diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/metrics.snap index 8da724b4c2..49532e0f8f 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/metrics.snap +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/metrics.snap @@ -10,7 +10,8 @@ info: cache: apollo.router.operations.entity.cache: attributes: - entity.type: true + entity.type: + alias: entity_type subgraph.name: subgraph_name: true supergraph.operation.name: @@ -63,25 +64,25 @@ info: - value: 0 attributes: cache.hit: false - entity.type: Product + entity_type: Product subgraph.name: products supergraph.operation.name: Test - value: 0 attributes: cache.hit: false - entity.type: Review + entity_type: Review subgraph.name: products supergraph.operation.name: Test - value: 3 attributes: cache.hit: true - entity.type: Product + entity_type: Product subgraph.name: products supergraph.operation.name: Test - value: 5 attributes: cache.hit: true - entity.type: Review + entity_type: Review subgraph.name: products supergraph.operation.name: Test - name: only_cache_hit_on_subgraph_products diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/router.yaml index b6eee5fb07..f140a9f1e9 100644 --- a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/router.yaml +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/caching/router.yaml @@ -5,7 +5,8 @@ telemetry: cache: apollo.router.operations.entity.cache: attributes: - entity.type: true + graphql.type.name: + alias: entity_type subgraph.name: subgraph_name: true supergraph.operation.name: diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/metrics.snap b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/metrics.snap new file mode 100644 index 0000000000..ae36e59b7d --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/metrics.snap @@ -0,0 +1,23 @@ +--- +source: apollo-router/src/plugins/telemetry/config_new/instruments.rs +description: standard instrument http.client.request.duration +expression: "&metrics.all()" +info: + telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + http.client.request.duration: + attributes: + subgraph.name: + alias: apollo_subgraph_name +--- +- name: http.client.request.duration + description: Duration of HTTP client requests. + unit: s + data: + datapoints: + - sum: 0.1 + attributes: + apollo_subgraph_name: products diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/router.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/router.yaml new file mode 100644 index 0000000000..5eb31bceb4 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/router.yaml @@ -0,0 +1,9 @@ +telemetry: + instrumentation: + instruments: + default_requirement_level: none + subgraph: + http.client.request.duration: + attributes: + subgraph.name: + alias: apollo_subgraph_name \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/test.yaml b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/test.yaml new file mode 100644 index 0000000000..867053ecd4 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/fixtures/subgraph/custom_histogram_duration copy/test.yaml @@ -0,0 +1,30 @@ +description: standard instrument http.client.request.duration +events: + - - router_request: + uri: "/hello" + method: GET + body: | + hello + - supergraph_request: + uri: "/hello" + method: GET + headers: + custom_header: custom_value + query: "query { hello }" + - subgraph_request: + query: "query { hello }" + operation_name: "Products" + operation_kind: query + subgraph_name: "products" + - subgraph_response: + status: 200 + data: + hello: "world" + - supergraph_response: + status: 200 + data: + hello: "world" + - router_response: + body: | + hello + status: 200 \ No newline at end of file diff --git a/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs b/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs index e0267f1482..8765348d78 100644 --- a/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs +++ b/apollo-router/src/plugins/telemetry/config_new/graphql/attributes.rs @@ -1,11 +1,13 @@ use apollo_compiler::executable::Field; use apollo_compiler::executable::NamedType; +use opentelemetry::Key; use opentelemetry_api::KeyValue; use schemars::JsonSchema; use serde::Deserialize; use serde_json_bytes::Value; use tower::BoxError; +use crate::plugins::telemetry::config_new::attributes::StandardAttribute; use crate::plugins::telemetry::config_new::graphql::selectors::FieldName; use crate::plugins::telemetry::config_new::graphql::selectors::FieldType; use crate::plugins::telemetry::config_new::graphql::selectors::GraphQLSelector; @@ -25,19 +27,19 @@ use crate::Context; pub(crate) struct GraphQLAttributes { /// The GraphQL field name #[serde(rename = "graphql.field.name")] - pub(crate) field_name: Option, + pub(crate) field_name: Option, /// The GraphQL field type #[serde(rename = "graphql.field.type")] - pub(crate) field_type: Option, + pub(crate) field_type: Option, /// If the field is a list, the length of the list #[serde(rename = "graphql.list.length")] - pub(crate) list_length: Option, + pub(crate) list_length: Option, /// The GraphQL operation name #[serde(rename = "graphql.operation.name")] - pub(crate) operation_name: Option, + pub(crate) operation_name: Option, /// The GraphQL type name #[serde(rename = "graphql.type.name")] - pub(crate) type_name: Option, + pub(crate) type_name: Option, } impl DefaultForLevel for GraphQLAttributes { @@ -48,9 +50,9 @@ impl DefaultForLevel for GraphQLAttributes { ) { if let TelemetryDataKind::Metrics = kind { if let DefaultAttributeRequirementLevel::Required = requirement_level { - self.field_name.get_or_insert(true); - self.field_type.get_or_insert(true); - self.type_name.get_or_insert(true); + self.field_name.get_or_insert(StandardAttribute::Bool(true)); + self.field_type.get_or_insert(StandardAttribute::Bool(true)); + self.type_name.get_or_insert(StandardAttribute::Bool(true)); } } } @@ -81,50 +83,70 @@ impl Selectors for GraphQLAttributes { value: &Value, ctx: &Context, ) { - if let Some(true) = self.field_name { + if let Some(key) = self + .field_name + .as_ref() + .and_then(|a| a.key(Key::from_static_str("graphql.field.name"))) + { if let Some(name) = (GraphQLSelector::FieldName { field_name: FieldName::String, }) .on_response_field(ty, field, value, ctx) { - attrs.push(KeyValue::new("graphql.field.name", name)); + attrs.push(KeyValue::new(key, name)); } } - if let Some(true) = self.field_type { + if let Some(key) = self + .field_type + .as_ref() + .and_then(|a| a.key(Key::from_static_str("graphql.field.type"))) + { if let Some(ty) = (GraphQLSelector::FieldType { field_type: FieldType::Name, }) .on_response_field(ty, field, value, ctx) { - attrs.push(KeyValue::new("graphql.field.type", ty)); + attrs.push(KeyValue::new(key, ty)); } } - if let Some(true) = self.type_name { + if let Some(key) = self + .type_name + .as_ref() + .and_then(|a| a.key(Key::from_static_str("graphql.type.name"))) + { if let Some(ty) = (GraphQLSelector::TypeName { type_name: TypeName::String, }) .on_response_field(ty, field, value, ctx) { - attrs.push(KeyValue::new("graphql.type.name", ty)); + attrs.push(KeyValue::new(key, ty)); } } - if let Some(true) = self.list_length { + if let Some(key) = self + .list_length + .as_ref() + .and_then(|a| a.key(Key::from_static_str("graphql.list.length"))) + { if let Some(length) = (GraphQLSelector::ListLength { list_length: ListLength::Value, }) .on_response_field(ty, field, value, ctx) { - attrs.push(KeyValue::new("graphql.list.length", length)); + attrs.push(KeyValue::new(key, length)); } } - if let Some(true) = self.operation_name { + if let Some(key) = self + .operation_name + .as_ref() + .and_then(|a| a.key(Key::from_static_str("graphql.operation.name"))) + { if let Some(length) = (GraphQLSelector::OperationName { operation_name: OperationName::String, default: None, }) .on_response_field(ty, field, value, ctx) { - attrs.push(KeyValue::new("graphql.operation.name", length)); + attrs.push(KeyValue::new(key, length)); } } } @@ -135,6 +157,7 @@ mod test { use serde_json_bytes::json; use crate::context::OPERATION_NAME; + use crate::plugins::telemetry::config_new::attributes::StandardAttribute; use crate::plugins::telemetry::config_new::test::field; use crate::plugins::telemetry::config_new::test::ty; use crate::plugins::telemetry::config_new::DefaultForLevel; @@ -148,9 +171,9 @@ mod test { super::DefaultAttributeRequirementLevel::Required, super::TelemetryDataKind::Metrics, ); - assert_eq!(attributes.field_name, Some(true)); - assert_eq!(attributes.field_type, Some(true)); - assert_eq!(attributes.type_name, Some(true)); + assert_eq!(attributes.field_name, Some(StandardAttribute::Bool(true))); + assert_eq!(attributes.field_type, Some(StandardAttribute::Bool(true))); + assert_eq!(attributes.type_name, Some(StandardAttribute::Bool(true))); assert_eq!(attributes.list_length, None); assert_eq!(attributes.operation_name, None); } @@ -158,11 +181,11 @@ mod test { #[test] fn test_on_response_field_non_list() { let attributes = super::GraphQLAttributes { - field_name: Some(true), - field_type: Some(true), - list_length: Some(true), - operation_name: Some(true), - type_name: Some(true), + field_name: Some(StandardAttribute::Bool(true)), + field_type: Some(StandardAttribute::Bool(true)), + list_length: Some(StandardAttribute::Bool(true)), + operation_name: Some(StandardAttribute::Bool(true)), + type_name: Some(StandardAttribute::Bool(true)), }; let ctx = Context::default(); let _ = ctx.insert(OPERATION_NAME, "operation_name".to_string()); @@ -182,11 +205,11 @@ mod test { #[test] fn test_on_response_field_list() { let attributes = super::GraphQLAttributes { - field_name: Some(true), - field_type: Some(true), - list_length: Some(true), - operation_name: Some(true), - type_name: Some(true), + field_name: Some(StandardAttribute::Bool(true)), + field_type: Some(StandardAttribute::Bool(true)), + list_length: Some(StandardAttribute::Bool(true)), + operation_name: Some(StandardAttribute::Bool(true)), + type_name: Some(StandardAttribute::Bool(true)), }; let ctx = Context::default(); let _ = ctx.insert(OPERATION_NAME, "operation_name".to_string()); diff --git a/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs b/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs index 7e68446e3d..3cabed05ff 100644 --- a/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs +++ b/apollo-router/src/plugins/telemetry/config_new/graphql/mod.rs @@ -9,6 +9,7 @@ use tower::BoxError; use super::instruments::CustomCounter; use super::instruments::CustomInstruments; use crate::graphql::ResponseVisitor; +use crate::json_ext::Object; use crate::plugins::telemetry::config_new::attributes::DefaultAttributeRequirementLevel; use crate::plugins::telemetry::config_new::extendable::Extendable; use crate::plugins::telemetry::config_new::graphql::attributes::GraphQLAttributes; @@ -136,7 +137,13 @@ impl Instrumented for GraphQLInstruments { ctx, instruments: self, } - .visit(&executable_document, response); + .visit( + &executable_document, + response, + &ctx.get_demand_control_context() + .map(|c| c.variables) + .unwrap_or_default(), + ); } } } @@ -161,6 +168,7 @@ impl<'a> ResponseVisitor for GraphQLInstrumentsVisitor<'a> { fn visit_field( &mut self, request: &ExecutableDocument, + variables: &Object, ty: &NamedType, field: &Field, value: &Value, @@ -171,11 +179,17 @@ impl<'a> ResponseVisitor for GraphQLInstrumentsVisitor<'a> { match value { Value::Array(items) => { for item in items { - self.visit_list_item(request, field.ty().inner_named_type(), field, item); + self.visit_list_item( + request, + variables, + field.ty().inner_named_type(), + field, + item, + ); } } Value::Object(children) => { - self.visit_selections(request, &field.selection_set, children); + self.visit_selections(request, variables, &field.selection_set, children); } _ => {} } diff --git a/apollo-router/src/plugins/telemetry/config_new/selectors.rs b/apollo-router/src/plugins/telemetry/config_new/selectors.rs index 9047764a80..8c9c8dde7a 100644 --- a/apollo-router/src/plugins/telemetry/config_new/selectors.rs +++ b/apollo-router/src/plugins/telemetry/config_new/selectors.rs @@ -14,7 +14,6 @@ use crate::plugin::serde::deserialize_json_query; use crate::plugin::serde::deserialize_jsonpath; use crate::plugins::cache::entity::CacheSubgraph; use crate::plugins::cache::metrics::CacheMetricContextKey; -use crate::plugins::demand_control::CostContext; use crate::plugins::telemetry::config::AttributeValue; use crate::plugins::telemetry::config::TraceIdFormat; use crate::plugins::telemetry::config_new::cost::CostValue; @@ -1090,14 +1089,28 @@ impl Selector for SupergraphSelector { val.maybe_to_otel_value() } .or_else(|| default.maybe_to_otel_value()), - SupergraphSelector::Cost { cost } => ctx.extensions().with_lock(|lock| { - lock.get::().map(|cost_result| match cost { - CostValue::Estimated => cost_result.estimated.into(), - CostValue::Actual => cost_result.actual.into(), - CostValue::Delta => cost_result.delta().into(), - CostValue::Result => cost_result.result.into(), - }) - }), + SupergraphSelector::Cost { cost } => match cost { + CostValue::Estimated => ctx + .get_estimated_cost() + .ok() + .flatten() + .map(opentelemetry::Value::from), + CostValue::Actual => ctx + .get_actual_cost() + .ok() + .flatten() + .map(opentelemetry::Value::from), + CostValue::Delta => ctx + .get_cost_delta() + .ok() + .flatten() + .map(opentelemetry::Value::from), + CostValue::Result => ctx + .get_cost_result() + .ok() + .flatten() + .map(opentelemetry::Value::from), + }, SupergraphSelector::OnGraphQLError { on_graphql_error } if *on_graphql_error => { if ctx.get_json_value(CONTAINS_GRAPHQL_ERROR) == Some(serde_json_bytes::Value::Bool(true)) diff --git a/apollo-router/src/plugins/telemetry/config_new/spans.rs b/apollo-router/src/plugins/telemetry/config_new/spans.rs index 61dfb0f35c..0abf12b797 100644 --- a/apollo-router/src/plugins/telemetry/config_new/spans.rs +++ b/apollo-router/src/plugins/telemetry/config_new/spans.rs @@ -140,9 +140,11 @@ mod test { use serde_json_bytes::path::JsonPathInst; use crate::context::CONTAINS_GRAPHQL_ERROR; + use crate::context::OPERATION_KIND; use crate::graphql; use crate::plugins::telemetry::config::AttributeValue; use crate::plugins::telemetry::config_new::attributes::DefaultAttributeRequirementLevel; + use crate::plugins::telemetry::config_new::attributes::StandardAttribute; use crate::plugins::telemetry::config_new::attributes::SUBGRAPH_GRAPHQL_DOCUMENT; use crate::plugins::telemetry::config_new::conditional::Conditional; use crate::plugins::telemetry::config_new::conditions::Condition; @@ -553,6 +555,24 @@ mod test { .any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))); } + #[test] + fn test_router_request_standard_attribute_aliased() { + let mut spans = RouterSpans::default(); + spans.attributes.attributes.common.http_request_method = Some(StandardAttribute::Aliased { + alias: String::from("my.method"), + }); + let values = spans.attributes.on_request( + &router::Request::fake_builder() + .method(http::Method::POST) + .header("my-header", "test_val") + .build() + .unwrap(), + ); + assert!(values + .iter() + .any(|key_val| key_val.key == opentelemetry::Key::from_static_str("my.method"))); + } + #[test] fn test_router_response_custom_attribute() { let mut spans = RouterSpans::default(); @@ -620,6 +640,28 @@ mod test { .any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))); } + #[test] + fn test_supergraph_standard_attribute_aliased() { + let mut spans = SupergraphSpans::default(); + spans.attributes.attributes.graphql_operation_type = Some(StandardAttribute::Aliased { + alias: String::from("my_op"), + }); + let context = Context::new(); + context.insert(OPERATION_KIND, "Query".to_string()).unwrap(); + let values = spans.attributes.on_request( + &supergraph::Request::fake_builder() + .method(http::Method::POST) + .header("my-header", "test_val") + .query("Query { me { id } }") + .context(context) + .build() + .unwrap(), + ); + assert!(values + .iter() + .any(|key_val| key_val.key == opentelemetry::Key::from_static_str("my_op"))); + } + #[test] fn test_supergraph_response_event_custom_attribute() { let mut spans = SupergraphSpans::default(); diff --git a/apollo-router/src/plugins/telemetry/metrics/local_type_stats.rs b/apollo-router/src/plugins/telemetry/metrics/local_type_stats.rs index f53e8667be..b586dacf5c 100644 --- a/apollo-router/src/plugins/telemetry/metrics/local_type_stats.rs +++ b/apollo-router/src/plugins/telemetry/metrics/local_type_stats.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use crate::graphql::ResponseVisitor; +use crate::json_ext::Object; use crate::plugins::telemetry::metrics::apollo::histogram::ListLengthHistogram; use crate::plugins::telemetry::metrics::apollo::studio::LocalFieldStat; use crate::plugins::telemetry::metrics::apollo::studio::LocalTypeStat; @@ -22,6 +23,7 @@ impl ResponseVisitor for LocalTypeStatRecorder { fn visit_field( &mut self, request: &apollo_compiler::ExecutableDocument, + variables: &Object, ty: &apollo_compiler::executable::NamedType, field: &apollo_compiler::executable::Field, value: &serde_json_bytes::Value, @@ -61,11 +63,17 @@ impl ResponseVisitor for LocalTypeStatRecorder { .record(Some(items.len() as u64), 1); for item in items { - self.visit_list_item(request, field.ty().inner_named_type(), field, item); + self.visit_list_item( + request, + variables, + field.ty().inner_named_type(), + field, + item, + ); } } serde_json_bytes::Value::Object(children) => { - self.visit_selections(request, &field.selection_set, children); + self.visit_selections(request, variables, &field.selection_set, children); } _ => {} } diff --git a/apollo-router/src/plugins/telemetry/mod.rs b/apollo-router/src/plugins/telemetry/mod.rs index 7fdc8cf496..1478261be5 100644 --- a/apollo-router/src/plugins/telemetry/mod.rs +++ b/apollo-router/src/plugins/telemetry/mod.rs @@ -95,7 +95,6 @@ use crate::metrics::filter::FilterMeterProvider; use crate::metrics::meter_provider; use crate::plugin::Plugin; use crate::plugin::PluginInit; -use crate::plugins::demand_control; use crate::plugins::telemetry::apollo::ForwardHeaders; use crate::plugins::telemetry::apollo_exporter::proto::reports::trace::node::Id::ResponseName; use crate::plugins::telemetry::apollo_exporter::proto::reports::StatsContext; @@ -1411,7 +1410,13 @@ impl Telemetry { config.apollo.experimental_local_field_metrics, ctx.unsupported_executable_document(), ) { - local_stat_recorder.visit(&query, &response); + local_stat_recorder.visit( + &query, + &response, + &ctx.get_demand_control_context() + .map(|c| c.variables) + .unwrap_or_default(), + ); } if operation_kind == OperationKind::Subscription { @@ -1514,14 +1519,13 @@ impl Telemetry { let traces = Self::subgraph_ftv1_traces(context); let per_type_stat = Self::per_type_stat(&traces, field_level_instrumentation_ratio); let root_error_stats = Self::per_path_error_stats(&traces); + let strategy = context.get_demand_control_context().map(|c| c.strategy); let limits_stats = context.extensions().with_lock(|guard| { - let strategy = guard.get::(); - let cost_ctx = guard.get::(); let query_limits = guard.get::>(); SingleLimitsStats { strategy: strategy.and_then(|s| serde_json::to_string(&s.mode).ok()), - cost_estimated: cost_ctx.map(|ctx| ctx.estimated), - cost_actual: cost_ctx.map(|ctx| ctx.actual), + cost_estimated: context.get_estimated_cost().ok().flatten(), + cost_actual: context.get_actual_cost().ok().flatten(), // These limits are related to the Traffic Shaping feature, unrelated to the Demand Control plugin depth: query_limits.map_or(0, |ql| ql.depth as u64), @@ -2176,8 +2180,11 @@ mod tests { use crate::plugin::test::MockSubgraphService; use crate::plugin::test::MockSupergraphService; use crate::plugin::DynPlugin; - use crate::plugins::demand_control::CostContext; use crate::plugins::demand_control::DemandControlError; + use crate::plugins::demand_control::COST_ACTUAL_KEY; + use crate::plugins::demand_control::COST_ESTIMATED_KEY; + use crate::plugins::demand_control::COST_RESULT_KEY; + use crate::plugins::demand_control::COST_STRATEGY_KEY; use crate::plugins::telemetry::config::TraceIdFormat; use crate::plugins::telemetry::handle_error_internal; use crate::services::router::body::get_body_bytes; @@ -3249,6 +3256,14 @@ mod tests { ); } + #[derive(Clone)] + struct CostContext { + pub(crate) estimated: f64, + pub(crate) actual: f64, + pub(crate) result: &'static str, + pub(crate) strategy: &'static str, + } + async fn make_failed_demand_control_request(plugin: &dyn DynPlugin, cost_details: CostContext) { let mut mock_service = MockSupergraphService::new(); mock_service @@ -3258,6 +3273,18 @@ mod tests { req.context.extensions().with_lock(|mut lock| { lock.insert(cost_details.clone()); }); + req.context + .insert(COST_ESTIMATED_KEY, cost_details.estimated) + .unwrap(); + req.context + .insert(COST_ACTUAL_KEY, cost_details.actual) + .unwrap(); + req.context + .insert(COST_RESULT_KEY, cost_details.result.to_string()) + .unwrap(); + req.context + .insert(COST_STRATEGY_KEY, cost_details.strategy.to_string()) + .unwrap(); let errors = if cost_details.result == "COST_ESTIMATED_TOO_EXPENSIVE" { DemandControlError::EstimatedCostTooExpensive { diff --git a/apollo-router/src/plugins/telemetry/reload.rs b/apollo-router/src/plugins/telemetry/reload.rs index 0f529efd2f..2ca69191c9 100644 --- a/apollo-router/src/plugins/telemetry/reload.rs +++ b/apollo-router/src/plugins/telemetry/reload.rs @@ -35,7 +35,6 @@ use crate::plugins::telemetry::formatters::FilteringFormatter; use crate::plugins::telemetry::otel; use crate::plugins::telemetry::otel::OpenTelemetryLayer; use crate::plugins::telemetry::otel::PreSampledTracer; -use crate::plugins::telemetry::tracing::datadog_exporter::DatadogTraceState; use crate::plugins::telemetry::tracing::reload::ReloadTracer; use crate::tracer::TraceId; @@ -141,9 +140,7 @@ pub(crate) fn prepare_context(context: Context) -> Context { tracer.new_span_id(), TraceFlags::default(), false, - TraceState::default() - .with_measuring(true) - .with_priority_sampling(true), + TraceState::default(), ); return context.with_remote_span_context(span_context); } diff --git a/apollo-router/src/plugins/test.rs b/apollo-router/src/plugins/test.rs index 50ceac0110..1593523015 100644 --- a/apollo-router/src/plugins/test.rs +++ b/apollo-router/src/plugins/test.rs @@ -10,6 +10,7 @@ use tower::BoxError; use tower::ServiceBuilder; use tower_service::Service; +use crate::introspection::default_cache_storage; use crate::plugin::DynPlugin; use crate::plugin::Plugin; use crate::plugin::PluginInit; @@ -95,10 +96,15 @@ impl PluginTestHarness { let sdl = schema.raw_sdl.clone(); let supergraph = schema.supergraph_schema().clone(); let rust_planner = PlannerMode::maybe_rust(&schema, &config).unwrap(); - let planner = - BridgeQueryPlanner::new(schema.into(), Arc::new(config), None, rust_planner) - .await - .unwrap(); + let planner = BridgeQueryPlanner::new( + schema.into(), + Arc::new(config), + None, + rust_planner, + default_cache_storage().await, + ) + .await + .unwrap(); (sdl, supergraph, planner.subgraph_schemas()) } else { ( diff --git a/apollo-router/src/plugins/traffic_shaping/mod.rs b/apollo-router/src/plugins/traffic_shaping/mod.rs index e4e2eabfa1..e77322fffa 100644 --- a/apollo-router/src/plugins/traffic_shaping/mod.rs +++ b/apollo-router/src/plugins/traffic_shaping/mod.rs @@ -571,6 +571,7 @@ mod test { let config = Arc::new(config); let schema = Arc::new(Schema::parse(schema, &config).unwrap()); let planner = BridgeQueryPlannerPool::new( + Vec::new(), schema.clone(), config.clone(), NonZeroUsize::new(1).unwrap(), diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 4d94335e81..9cab43aab8 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -1,6 +1,5 @@ //! Calls out to nodejs query planner -use std::cmp::Ordering; use std::collections::HashMap; use std::fmt::Debug; use std::fmt::Write; @@ -13,6 +12,7 @@ use apollo_compiler::validation::Valid; use apollo_compiler::Name; use apollo_federation::error::FederationError; use apollo_federation::error::SingleFederationError; +use apollo_federation::query_plan::query_planner::QueryPlanOptions; use apollo_federation::query_plan::query_planner::QueryPlanner; use futures::future::BoxFuture; use opentelemetry_api::metrics::MeterProvider as _; @@ -30,7 +30,8 @@ use tower::Service; use super::PlanNode; use super::QueryKey; use crate::apollo_studio_interop::generate_usage_reporting; -use crate::configuration::IntrospectionMode; +use crate::cache::storage::CacheStorage; +use crate::configuration::IntrospectionMode as IntrospectionConfig; use crate::configuration::QueryPlannerMode; use crate::error::PlanErrors; use crate::error::QueryPlannerError; @@ -38,6 +39,7 @@ use crate::error::SchemaError; use crate::error::ServiceBuildError; use crate::error::ValidationErrors; use crate::graphql; +use crate::graphql::Response; use crate::introspection::Introspection; use crate::json_ext::Object; use crate::json_ext::Path; @@ -65,9 +67,8 @@ use crate::spec::SpecError; use crate::Configuration; pub(crate) const RUST_QP_MODE: &str = "rust"; -const JS_QP_MODE: &str = "js"; +pub(crate) const JS_QP_MODE: &str = "js"; const UNSUPPORTED_CONTEXT: &str = "context"; -const UNSUPPORTED_OVERRIDES: &str = "overrides"; const UNSUPPORTED_FED1: &str = "fed1"; const INTERNAL_INIT_ERROR: &str = "internal"; @@ -79,7 +80,7 @@ pub(crate) struct BridgeQueryPlanner { planner: PlannerMode, schema: Arc, subgraph_schemas: Arc>>>, - introspection: Option>, + introspection: IntrospectionMode, configuration: Arc, enable_authorization_directives: bool, _federation_instrument: ObservableGauge, @@ -93,11 +94,15 @@ pub(crate) enum PlannerMode { js: Arc>, rust: Arc, }, - Rust { - rust: Arc, - // TODO: remove when those other uses are fully ported to Rust - js_for_api_schema_and_introspection_and_operation_signature: Arc>, - }, + Rust(Arc), +} + +#[derive(Clone)] +enum IntrospectionMode { + Js(Arc), + Both(Arc), + Rust, + Disabled, } fn federation_version_instrument(federation_version: Option) -> ObservableGauge { @@ -120,25 +125,19 @@ impl PlannerMode { async fn new( schema: &Schema, configuration: &Configuration, - old_planner: Option>>, + old_planner: &Option>>, rust_planner: Option>, ) -> Result { Ok(match configuration.experimental_query_planner_mode { - QueryPlannerMode::New => Self::Rust { - js_for_api_schema_and_introspection_and_operation_signature: Self::js( - &schema.raw_sdl, - configuration, - old_planner, - ) - .await?, - rust: rust_planner + QueryPlannerMode::New => Self::Rust( + rust_planner .expect("expected Rust QP instance for `experimental_query_planner_mode: new`"), - }, + ), QueryPlannerMode::Legacy => { - Self::Js(Self::js(&schema.raw_sdl, configuration, old_planner).await?) + Self::Js(Self::js_planner(&schema.raw_sdl, configuration, old_planner).await?) } QueryPlannerMode::Both => Self::Both { - js: Self::js(&schema.raw_sdl, configuration, old_planner).await?, + js: Self::js_planner(&schema.raw_sdl, configuration, old_planner).await?, rust: rust_planner.expect( "expected Rust QP instance for `experimental_query_planner_mode: both`", ), @@ -146,11 +145,11 @@ impl PlannerMode { QueryPlannerMode::BothBestEffort => { if let Some(rust) = rust_planner { Self::Both { - js: Self::js(&schema.raw_sdl, configuration, old_planner).await?, + js: Self::js_planner(&schema.raw_sdl, configuration, old_planner).await?, rust, } } else { - Self::Js(Self::js(&schema.raw_sdl, configuration, old_planner).await?) + Self::Js(Self::js_planner(&schema.raw_sdl, configuration, old_planner).await?) } } }) @@ -190,6 +189,7 @@ impl PlannerMode { apollo_federation::query_plan::query_planner::QueryPlanIncrementalDeliveryConfig { enable_defer: configuration.supergraph.defer_support, }, + type_conditioned_fetching: configuration.experimental_type_conditioned_fetching, debug: Default::default(), }; let result = QueryPlanner::new(schema.federation_supergraph(), config); @@ -203,9 +203,6 @@ impl PlannerMode { metric_rust_qp_init(Some(UNSUPPORTED_FED1)); } SingleFederationError::UnsupportedFeature { message: _, kind } => match kind { - apollo_federation::error::UnsupportedFeatureKind::ProgressiveOverrides => { - metric_rust_qp_init(Some(UNSUPPORTED_OVERRIDES)) - } apollo_federation::error::UnsupportedFeatureKind::Context => { metric_rust_qp_init(Some(UNSUPPORTED_CONTEXT)) } @@ -222,13 +219,13 @@ impl PlannerMode { Ok(Arc::new(result.map_err(ServiceBuildError::QpInitError)?)) } - async fn js( + async fn js_planner( sdl: &str, configuration: &Configuration, - old_planner: Option>>, + old_js_planner: &Option>>, ) -> Result>, ServiceBuildError> { let query_planner_configuration = configuration.js_query_planner_config(); - let planner = match old_planner { + let planner = match old_js_planner { None => Planner::new(sdl.to_owned(), query_planner_configuration).await?, Some(old_planner) => { old_planner @@ -239,17 +236,25 @@ impl PlannerMode { Ok(Arc::new(planner)) } - fn js_for_api_schema_and_introspection_and_operation_signature( + async fn js_introspection( &self, - ) -> &Arc> { - match self { - PlannerMode::Js(js) => js, - PlannerMode::Both { js, .. } => js, - PlannerMode::Rust { - js_for_api_schema_and_introspection_and_operation_signature, - .. - } => js_for_api_schema_and_introspection_and_operation_signature, - } + sdl: &str, + configuration: &Configuration, + old_js_planner: &Option>>, + cache: CacheStorage, + ) -> Result, ServiceBuildError> { + let js_planner = match self { + Self::Js(js) => js.clone(), + Self::Both { js, .. } => js.clone(), + Self::Rust(_) => { + // JS "planner" (actually runtime) was not created for planning + // but is still needed for introspection, so create it now + Self::js_planner(sdl, configuration, old_js_planner).await? + } + }; + Ok(Arc::new( + Introspection::with_cache(js_planner, cache).await?, + )) } async fn plan( @@ -269,7 +274,8 @@ impl PlannerMode { let result = js.plan(filtered_query, operation, plan_options).await; - metric_query_planning_plan_duration(JS_QP_MODE, start); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_duration(JS_QP_MODE, elapsed); let mut success = result .map_err(QueryPlannerError::RouterBridgeError)? @@ -283,19 +289,42 @@ impl PlannerMode { } Ok(success) } - PlannerMode::Rust { rust, .. } => { - let start = Instant::now(); + PlannerMode::Rust(rust_planner) => { + let doc = doc.clone(); + let rust_planner = rust_planner.clone(); + let (plan, mut root_node) = tokio::task::spawn_blocking(move || { + let start = Instant::now(); + + let query_plan_options = QueryPlanOptions { + override_conditions: plan_options.override_conditions, + }; - let result = operation - .as_deref() - .map(|n| Name::new(n).map_err(FederationError::from)) - .transpose() - .and_then(|operation| rust.build_query_plan(&doc.executable, operation)) - .map_err(|e| QueryPlannerError::FederationError(e.to_string())); + let result = operation + .as_deref() + .map(|n| Name::new(n).map_err(FederationError::from)) + .transpose() + .and_then(|operation| { + rust_planner.build_query_plan( + &doc.executable, + operation, + query_plan_options, + ) + }) + .map_err(|e| QueryPlannerError::FederationError(e.to_string())); - metric_query_planning_plan_duration(RUST_QP_MODE, start); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); - let plan = result?; + result.map(|plan| { + let root_node = convert_root_query_plan_node(&plan); + (plan, root_node) + }) + }) + .await + .expect("query planner panicked")?; + if let Some(node) = &mut root_node { + init_query_plan_root_node(node)?; + } // Dummy value overwritten below in `BrigeQueryPlanner::plan` let usage_reporting = UsageReporting { @@ -303,11 +332,6 @@ impl PlannerMode { referenced_fields_by_type: Default::default(), }; - let mut root_node = convert_root_query_plan_node(&plan); - if let Some(node) = &mut root_node { - init_query_plan_root_node(node)?; - } - Ok(PlanSuccess { usage_reporting, data: QueryPlanResult { @@ -327,10 +351,11 @@ impl PlannerMode { let start = Instant::now(); let result = js - .plan(filtered_query, operation.clone(), plan_options) + .plan(filtered_query, operation.clone(), plan_options.clone()) .await; - metric_query_planning_plan_duration(JS_QP_MODE, start); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_duration(JS_QP_MODE, elapsed); let mut js_result = result .map_err(QueryPlannerError::RouterBridgeError)? @@ -345,8 +370,12 @@ impl PlannerMode { } } + let query_plan_options = QueryPlanOptions { + override_conditions: plan_options.override_conditions, + }; BothModeComparisonJob { rust_planner: rust.clone(), + js_duration: elapsed, document: doc.executable.clone(), operation_name: operation, // Exclude usage reporting from the Result sent for comparison @@ -354,6 +383,7 @@ impl PlannerMode { .as_ref() .map(|success| success.data.clone()) .map_err(|e| e.errors.clone()), + plan_options: query_plan_options, } .schedule(); @@ -368,7 +398,7 @@ impl PlannerMode { let js = match self { PlannerMode::Js(js) => js, PlannerMode::Both { js, .. } => js, - PlannerMode::Rust { rust, .. } => { + PlannerMode::Rust(rust) => { return Ok(rust .subgraph_schemas() .iter() @@ -394,23 +424,29 @@ impl BridgeQueryPlanner { configuration: Arc, old_js_planner: Option>>, rust_planner: Option>, + cache: CacheStorage, ) -> Result { let planner = - PlannerMode::new(&schema, &configuration, old_js_planner, rust_planner).await?; + PlannerMode::new(&schema, &configuration, &old_js_planner, rust_planner).await?; let subgraph_schemas = Arc::new(planner.subgraphs().await?); let introspection = if configuration.supergraph.introspection { - Some(Arc::new( - Introspection::new( + match configuration.experimental_introspection_mode { + IntrospectionConfig::New => IntrospectionMode::Rust, + IntrospectionConfig::Legacy => IntrospectionMode::Js( planner - .js_for_api_schema_and_introspection_and_operation_signature() - .clone(), - ) - .await?, - )) + .js_introspection(&schema.raw_sdl, &configuration, &old_js_planner, cache) + .await?, + ), + IntrospectionConfig::Both => IntrospectionMode::Both( + planner + .js_introspection(&schema.raw_sdl, &configuration, &old_js_planner, cache) + .await?, + ), + } } else { - None + IntrospectionMode::Disabled }; let enable_authorization_directives = @@ -431,10 +467,18 @@ impl BridgeQueryPlanner { }) } - pub(crate) fn planner(&self) -> Arc> { - self.planner - .js_for_api_schema_and_introspection_and_operation_signature() - .clone() + pub(crate) fn js_planner(&self) -> Option>> { + match &self.planner { + PlannerMode::Js(js) => Some(js.clone()), + PlannerMode::Both { js, .. } => Some(js.clone()), + PlannerMode::Rust(_) => match &self.introspection { + IntrospectionMode::Js(js_introspection) + | IntrospectionMode::Both(js_introspection) => { + Some(js_introspection.planner.clone()) + } + IntrospectionMode::Rust | IntrospectionMode::Disabled => None, + }, + } } #[cfg(test)] @@ -494,11 +538,23 @@ impl BridgeQueryPlanner { key: QueryKey, doc: ParsedDocument, ) -> Result { - let Some(introspection) = &self.introspection else { - return Ok(QueryPlannerContent::IntrospectionDisabled); - }; - let mode = self.configuration.experimental_introspection_mode; - let response = if mode != IntrospectionMode::New && doc.executable.operations.len() > 1 { + match &self.introspection { + IntrospectionMode::Disabled => return Ok(QueryPlannerContent::IntrospectionDisabled), + IntrospectionMode::Rust => { + let schema = self.schema.clone(); + let response = Box::new( + tokio::task::spawn_blocking(move || { + Self::rust_introspection(&schema, &key, &doc) + }) + .await + .expect("Introspection panicked")?, + ); + return Ok(QueryPlannerContent::Response { response }); + } + IntrospectionMode::Js(_) | IntrospectionMode::Both(_) => {} + } + + if doc.executable.operations.len() > 1 { // TODO: add an operation_name parameter to router-bridge to fix this? let error = graphql::Error::builder() .message( @@ -510,15 +566,23 @@ impl BridgeQueryPlanner { return Ok(QueryPlannerContent::Response { response: Box::new(graphql::Response::builder().error(error).build()), }); - } else { - match mode { - IntrospectionMode::Legacy => introspection - .execute(key.filtered_query) + } + + let response = match &self.introspection { + IntrospectionMode::Rust | IntrospectionMode::Disabled => unreachable!(), // returned above + IntrospectionMode::Js(js) => js + .execute(key.filtered_query) + .await + .map_err(QueryPlannerError::Introspection)?, + IntrospectionMode::Both(js) => { + let js_result = js + .execute(key.filtered_query.clone()) .await - .map_err(QueryPlannerError::Introspection)?, - IntrospectionMode::New => self.rust_introspection(&key, &doc)?, - IntrospectionMode::Both => { - let rust_result = match self.rust_introspection(&key, &doc) { + .map_err(QueryPlannerError::Introspection); + let schema = self.schema.clone(); + let js_result_clone = js_result.clone(); + tokio::task::spawn_blocking(move || { + let rust_result = match Self::rust_introspection(&schema, &key, &doc) { Ok(response) => { if response.errors.is_empty() { Ok(response) @@ -537,27 +601,28 @@ impl BridgeQueryPlanner { } Err(e) => Err(e), }; - let js_result = introspection - .execute(key.filtered_query) - .await - .map_err(QueryPlannerError::Introspection); - self.compare_introspection_responses(js_result.clone(), rust_result); - js_result? - } + super::dual_introspection::compare_introspection_responses( + &key.original_query, + js_result_clone, + rust_result, + ); + }) + .await + .expect("Introspection comparison panicked"); + js_result? } }; - Ok(QueryPlannerContent::Response { response: Box::new(response), }) } fn rust_introspection( - &self, + schema: &Schema, key: &QueryKey, doc: &ParsedDocument, ) -> Result { - let schema = self.schema.api_schema(); + let schema = schema.api_schema(); let operation = doc.get_operation(key.operation_name.as_deref())?; let variable_values = Default::default(); let variable_values = @@ -580,138 +645,6 @@ impl BridgeQueryPlanner { Ok(response.into()) } - fn compare_introspection_responses( - &self, - mut js_result: Result, - mut rust_result: Result, - ) { - let is_matched; - match (&mut js_result, &mut rust_result) { - (Err(_), Err(_)) => { - is_matched = true; - } - (Err(err), Ok(_)) => { - is_matched = false; - tracing::warn!("JS introspection error: {err}") - } - (Ok(_), Err(err)) => { - is_matched = false; - tracing::warn!("Rust introspection error: {err}") - } - (Ok(js_response), Ok(rust_response)) => { - if let (Some(js_data), Some(rust_data)) = - (&mut js_response.data, &mut rust_response.data) - { - json_sort_arrays(js_data); - json_sort_arrays(rust_data); - } - is_matched = js_response.data == rust_response.data; - if is_matched { - tracing::debug!("Introspection match! 🎉") - } else { - tracing::debug!("Introspection mismatch"); - tracing::trace!("Introspection diff:\n{}", { - let rust = rust_response - .data - .as_ref() - .map(|d| serde_json::to_string_pretty(&d).unwrap()) - .unwrap_or_default(); - let js = js_response - .data - .as_ref() - .map(|d| serde_json::to_string_pretty(&d).unwrap()) - .unwrap_or_default(); - let diff = similar::TextDiff::from_lines(&js, &rust); - diff.unified_diff() - .context_radius(10) - .header("JS", "Rust") - .to_string() - }) - } - } - } - - u64_counter!( - "apollo.router.operations.introspection.both", - "Comparing JS v.s. Rust introspection", - 1, - "generation.is_matched" = is_matched, - "generation.js_error" = js_result.is_err(), - "generation.rust_error" = rust_result.is_err() - ); - - fn json_sort_arrays(value: &mut Value) { - match value { - Value::Array(array) => { - for item in array.iter_mut() { - json_sort_arrays(item) - } - array.sort_by(json_compare) - } - Value::Object(object) => { - for (_key, value) in object { - json_sort_arrays(value) - } - } - Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {} - } - } - - fn json_compare(a: &Value, b: &Value) -> Ordering { - match (a, b) { - (Value::Null, Value::Null) => Ordering::Equal, - (Value::Bool(a), Value::Bool(b)) => a.cmp(b), - (Value::Number(a), Value::Number(b)) => { - a.as_f64().unwrap().total_cmp(&b.as_f64().unwrap()) - } - (Value::String(a), Value::String(b)) => a.cmp(b), - (Value::Array(a), Value::Array(b)) => iter_cmp(a, b, json_compare), - (Value::Object(a), Value::Object(b)) => { - iter_cmp(a, b, |(key_a, a), (key_b, b)| { - debug_assert_eq!(key_a, key_b); // Response object keys are in selection set order - json_compare(a, b) - }) - } - _ => json_discriminant(a).cmp(&json_discriminant(b)), - } - } - - // TODO: use `Iterator::cmp_by` when available: - // https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.cmp_by - // https://github.com/rust-lang/rust/issues/64295 - fn iter_cmp( - a: impl IntoIterator, - b: impl IntoIterator, - cmp: impl Fn(T, T) -> Ordering, - ) -> Ordering { - use itertools::Itertools; - for either_or_both in a.into_iter().zip_longest(b) { - match either_or_both { - itertools::EitherOrBoth::Both(a, b) => { - let ordering = cmp(a, b); - if ordering != Ordering::Equal { - return ordering; - } - } - itertools::EitherOrBoth::Left(_) => return Ordering::Less, - itertools::EitherOrBoth::Right(_) => return Ordering::Greater, - } - } - Ordering::Equal - } - - fn json_discriminant(value: &Value) -> u8 { - match value { - Value::Null => 0, - Value::Bool(_) => 1, - Value::Number(_) => 2, - Value::String(_) => 3, - Value::Array(_) => 4, - Value::Object(_) => 5, - } - } - } - #[allow(clippy::too_many_arguments)] async fn plan( &self, @@ -937,6 +870,97 @@ impl BridgeQueryPlanner { mut key: QueryKey, mut doc: ParsedDocument, ) -> Result { + let mut query_metrics = Default::default(); + let mut selections = self + .parse_selections( + key.original_query.clone(), + key.operation_name.as_deref(), + &doc, + &mut query_metrics, + ) + .await?; + + if selections + .operation(key.operation_name.as_deref()) + .is_some_and(|op| op.selection_set.is_empty()) + { + // All selections have @skip(true) or @include(false) + // Return an empty response now to avoid dealing with an empty query plan later + return Ok(QueryPlannerContent::Response { + response: Box::new( + graphql::Response::builder() + .data(Value::Object(Default::default())) + .build(), + ), + }); + } + + { + let operation = doc + .executable + .operations + .get(key.operation_name.as_deref()) + .ok(); + let mut has_root_typename = false; + let mut has_schema_introspection = false; + let mut has_other_root_fields = false; + if let Some(operation) = operation { + for field in operation.root_fields(&doc.executable) { + match field.name.as_str() { + "__typename" => has_root_typename = true, + "__schema" | "__type" if operation.is_query() => { + has_schema_introspection = true + } + _ => has_other_root_fields = true, + } + } + if has_root_typename && !has_schema_introspection && !has_other_root_fields { + // Fast path for __typename alone + if operation + .selection_set + .selections + .iter() + .all(|sel| sel.as_field().is_some_and(|f| f.name == "__typename")) + { + let root_type_name: serde_json_bytes::ByteString = + operation.object_type().as_str().into(); + let data = Value::Object( + operation + .root_fields(&doc.executable) + .filter(|field| field.name == "__typename") + .map(|field| { + ( + field.response_key().as_str().into(), + Value::String(root_type_name.clone()), + ) + }) + .collect(), + ); + return Ok(QueryPlannerContent::Response { + response: Box::new(graphql::Response::builder().data(data).build()), + }); + } else { + // fragments might use @include or @skip + } + } + } else { + // Should be unreachable as QueryAnalysisLayer would have returned an error + } + + if has_schema_introspection { + if has_other_root_fields { + let error = graphql::Error::builder() + .message("Mixed queries with both schema introspection and concrete fields are not supported") + .extension_code("MIXED_INTROSPECTION") + .build(); + return Ok(QueryPlannerContent::Response { + response: Box::new(graphql::Response::builder().error(error).build()), + }); + } + return self.introspection(key, doc).await; + } + } + let filter_res = if self.enable_authorization_directives { match AuthorizationPlugin::filter_query(&self.configuration, &key, &self.schema) { Err(QueryPlannerError::Unauthorized(unauthorized_paths)) => { @@ -965,16 +989,6 @@ impl BridgeQueryPlanner { None }; - let mut query_metrics = Default::default(); - let mut selections = self - .parse_selections( - key.original_query.clone(), - key.operation_name.as_deref(), - &doc, - &mut query_metrics, - ) - .await?; - if let Some((unauthorized_paths, new_doc)) = filter_res { key.filtered_query = new_doc.to_string(); let executable_document = new_doc @@ -995,85 +1009,6 @@ impl BridgeQueryPlanner { selections.unauthorized.paths = unauthorized_paths; } - if selections - .operation(key.operation_name.as_deref()) - .is_some_and(|op| op.selection_set.is_empty()) - { - // All selections have @skip(true) or @include(false) - // Return an empty response now to avoid dealing with an empty query plan later - return Ok(QueryPlannerContent::Response { - response: Box::new( - graphql::Response::builder() - .data(Value::Object(Default::default())) - .build(), - ), - }); - } - - let operation = doc - .executable - .operations - .get(key.operation_name.as_deref()) - .ok(); - let mut has_root_typename = false; - let mut has_schema_introspection = false; - let mut has_other_root_fields = false; - if let Some(operation) = operation { - for field in operation.root_fields(&doc.executable) { - match field.name.as_str() { - "__typename" => has_root_typename = true, - "__schema" | "__type" if operation.is_query() => { - has_schema_introspection = true - } - _ => has_other_root_fields = true, - } - } - if has_root_typename && !has_schema_introspection && !has_other_root_fields { - // Fast path for __typename alone - if operation - .selection_set - .selections - .iter() - .all(|sel| sel.as_field().is_some_and(|f| f.name == "__typename")) - { - let root_type_name: serde_json_bytes::ByteString = - operation.object_type().as_str().into(); - let data = Value::Object( - operation - .root_fields(&doc.executable) - .filter(|field| field.name == "__typename") - .map(|field| { - ( - field.response_key().as_str().into(), - Value::String(root_type_name.clone()), - ) - }) - .collect(), - ); - return Ok(QueryPlannerContent::Response { - response: Box::new(graphql::Response::builder().data(data).build()), - }); - } else { - // fragments might use @include or @skip - } - } - } else { - // Should be unreachable as QueryAnalysisLayer would have returned an error - } - - if has_schema_introspection { - if has_other_root_fields { - let error = graphql::Error::builder() - .message("Mixed queries with both schema introspection and concrete fields are not supported") - .extension_code("MIXED_INTROSPECTION") - .build(); - return Ok(QueryPlannerContent::Response { - response: Box::new(graphql::Response::builder().error(error).build()), - }); - } - return self.introspection(key, doc).await; - } - if key.filtered_query != key.original_query { let mut filtered = self .parse_selections( @@ -1152,11 +1087,11 @@ pub fn render_diff(differences: &[diff::Result<&str>]) -> String { output } -pub(crate) fn metric_query_planning_plan_duration(planner: &'static str, start: Instant) { +pub(crate) fn metric_query_planning_plan_duration(planner: &'static str, elapsed: f64) { f64_histogram!( "apollo.router.query_planning.plan.duration", "Duration of the query planning.", - start.elapsed().as_secs_f64(), + elapsed, "planner" = planner ); } @@ -1191,6 +1126,7 @@ mod tests { use tower::ServiceExt; use super::*; + use crate::introspection::default_cache_storage; use crate::metrics::FutureMetricsExt as _; use crate::services::subgraph; use crate::services::supergraph; @@ -1232,12 +1168,18 @@ mod tests { #[test(tokio::test)] async fn federation_versions() { async { - let sdl = include_str!("../testdata/minimal_supergraph.graphql"); + let sdl = include_str!("../testdata/minimal_fed1_supergraph.graphql"); let config = Arc::default(); let schema = Schema::parse(sdl, &config).unwrap(); - let _planner = BridgeQueryPlanner::new(schema.into(), config, None, None) - .await - .unwrap(); + let _planner = BridgeQueryPlanner::new( + schema.into(), + config, + None, + None, + default_cache_storage().await, + ) + .await + .unwrap(); assert_gauge!( "apollo.router.supergraph.federation", @@ -1249,12 +1191,18 @@ mod tests { .await; async { - let sdl = include_str!("../testdata/minimal_fed2_supergraph.graphql"); + let sdl = include_str!("../testdata/minimal_supergraph.graphql"); let config = Arc::default(); let schema = Schema::parse(sdl, &config).unwrap(); - let _planner = BridgeQueryPlanner::new(schema.into(), config, None, None) - .await - .unwrap(); + let _planner = BridgeQueryPlanner::new( + schema.into(), + config, + None, + None, + default_cache_storage().await, + ) + .await + .unwrap(); assert_gauge!( "apollo.router.supergraph.federation", @@ -1271,9 +1219,15 @@ mod tests { let schema = Arc::new(Schema::parse(EXAMPLE_SCHEMA, &Default::default()).unwrap()); let query = include_str!("testdata/unknown_introspection_query.graphql"); - let planner = BridgeQueryPlanner::new(schema.clone(), Default::default(), None, None) - .await - .unwrap(); + let planner = BridgeQueryPlanner::new( + schema.clone(), + Default::default(), + None, + None, + default_cache_storage().await, + ) + .await + .unwrap(); let doc = Query::parse_document(query, None, &schema, &Configuration::default()).unwrap(); @@ -1297,8 +1251,8 @@ mod tests { &doc, query_metrics ) - .await - .unwrap_err(); + .await + .unwrap_err(); match err { QueryPlannerError::EmptyPlan(usage_reporting) => { @@ -1371,9 +1325,15 @@ mod tests { let configuration = Arc::new(configuration); let schema = Schema::parse(EXAMPLE_SCHEMA, &configuration).unwrap(); - let planner = BridgeQueryPlanner::new(schema.into(), configuration.clone(), None, None) - .await - .unwrap(); + let planner = BridgeQueryPlanner::new( + schema.into(), + configuration.clone(), + None, + None, + default_cache_storage().await, + ) + .await + .unwrap(); macro_rules! s { ($query: expr) => { @@ -1396,7 +1356,7 @@ mod tests { } }}"#); // Aliases - // FIXME: uncomment myName alias when this is fixed: + // FIXME: uncomment myName alias when this is fixed: // https://github.com/apollographql/router/issues/3263 s!(r#"query Q { me { username @@ -1679,9 +1639,15 @@ mod tests { let configuration = Arc::new(configuration); let schema = Schema::parse(schema, &configuration).unwrap(); - let planner = BridgeQueryPlanner::new(schema.into(), configuration.clone(), None, None) - .await - .unwrap(); + let planner = BridgeQueryPlanner::new( + schema.into(), + configuration.clone(), + None, + None, + default_cache_storage().await, + ) + .await + .unwrap(); let doc = Query::parse_document( original_query, @@ -1807,7 +1773,8 @@ mod tests { #[test] fn test_metric_query_planning_plan_duration() { let start = Instant::now(); - metric_query_planning_plan_duration(RUST_QP_MODE, start); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); assert_histogram_exists!( "apollo.router.query_planning.plan.duration", f64, @@ -1815,7 +1782,8 @@ mod tests { ); let start = Instant::now(); - metric_query_planning_plan_duration(JS_QP_MODE, start); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_duration(JS_QP_MODE, elapsed); assert_histogram_exists!( "apollo.router.query_planning.plan.duration", f64, @@ -1838,13 +1806,6 @@ mod tests { "init.error_kind" = "context", "init.is_success" = false ); - metric_rust_qp_init(Some(UNSUPPORTED_OVERRIDES)); - assert_counter!( - "apollo.router.lifecycle.query_planner.init", - 1, - "init.error_kind" = "overrides", - "init.is_success" = false - ); metric_rust_qp_init(Some(UNSUPPORTED_FED1)); assert_counter!( "apollo.router.lifecycle.query_planner.init", diff --git a/apollo-router/src/query_planner/bridge_query_planner_pool.rs b/apollo-router/src/query_planner/bridge_query_planner_pool.rs index bb75124df1..3200e59e7e 100644 --- a/apollo-router/src/query_planner/bridge_query_planner_pool.rs +++ b/apollo-router/src/query_planner/bridge_query_planner_pool.rs @@ -3,6 +3,7 @@ use std::num::NonZeroUsize; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; use std::sync::Arc; +use std::sync::Mutex; use std::time::Instant; use apollo_compiler::validation::Valid; @@ -12,7 +13,6 @@ use futures::future::BoxFuture; use opentelemetry::metrics::MeterProvider; use opentelemetry::metrics::ObservableGauge; use opentelemetry::metrics::Unit; -use opentelemetry_api::metrics::Meter; use router_bridge::planner::Planner; use tokio::sync::oneshot; use tokio::task::JoinSet; @@ -21,8 +21,11 @@ use tower::ServiceExt; use super::bridge_query_planner::BridgeQueryPlanner; use super::QueryPlanResult; +use crate::cache::storage::CacheStorage; use crate::error::QueryPlannerError; use crate::error::ServiceBuildError; +use crate::graphql::Response; +use crate::introspection::default_cache_storage; use crate::metrics::meter_provider; use crate::query_planner::PlannerMode; use crate::services::QueryPlannerRequest; @@ -41,23 +44,16 @@ pub(crate) struct BridgeQueryPlannerPool { )>, schema: Arc, subgraph_schemas: Arc>>>, - _pool_size_gauge: opentelemetry::metrics::ObservableGauge, + pool_size_gauge: Arc>>>, v8_heap_used: Arc, - _v8_heap_used_gauge: ObservableGauge, + v8_heap_used_gauge: Arc>>>, v8_heap_total: Arc, - _v8_heap_total_gauge: ObservableGauge, + v8_heap_total_gauge: Arc>>>, + introspection_cache: CacheStorage, } impl BridgeQueryPlannerPool { pub(crate) async fn new( - schema: Arc, - configuration: Arc, - size: NonZeroUsize, - ) -> Result { - Self::new_from_planners(Default::default(), schema, configuration, size).await - } - - pub(crate) async fn new_from_planners( old_js_planners: Vec>>, schema: Arc, configuration: Arc, @@ -74,16 +70,28 @@ impl BridgeQueryPlannerPool { let mut old_js_planners_iterator = old_js_planners.into_iter(); - (0..size.into()).for_each(|_| { + // All query planners in the pool now share the same introspection cache. + // This allows meaningful gauges, and it makes sense that queries should be cached across all planners. + let introspection_cache = default_cache_storage().await; + + for _ in 0..size.into() { let schema = schema.clone(); let configuration = configuration.clone(); let rust_planner = rust_planner.clone(); + let introspection_cache = introspection_cache.clone(); let old_planner = old_js_planners_iterator.next(); join_set.spawn(async move { - BridgeQueryPlanner::new(schema, configuration, old_planner, rust_planner).await + BridgeQueryPlanner::new( + schema, + configuration, + old_planner, + rust_planner, + introspection_cache, + ) + .await }); - }); + } let mut bridge_query_planners = Vec::new(); @@ -102,9 +110,9 @@ impl BridgeQueryPlannerPool { })? .subgraph_schemas(); - let planners: Vec<_> = bridge_query_planners + let js_planners: Vec<_> = bridge_query_planners .iter() - .map(|p| p.planner().clone()) + .filter_map(|p| p.js_planner()) .collect(); for mut planner in bridge_query_planners.into_iter() { @@ -127,20 +135,11 @@ impl BridgeQueryPlannerPool { } }); } - let sender_for_gauge = sender.clone(); - let meter = meter_provider().meter("apollo/router"); - let pool_size_gauge = meter - .u64_observable_gauge("apollo.router.query_planning.queued") - .with_description("Number of queries waiting to be planned") - .with_unit(Unit::new("query")) - .with_callback(move |m| m.observe(sender_for_gauge.len() as u64, &[])) - .init(); - - let (v8_heap_used, _v8_heap_used_gauge) = Self::create_heap_used_gauge(&meter); - let (v8_heap_total, _v8_heap_total_gauge) = Self::create_heap_total_gauge(&meter); + let v8_heap_used: Arc = Default::default(); + let v8_heap_total: Arc = Default::default(); // initialize v8 metrics - if let Some(bridge_query_planner) = planners.first().cloned() { + if let Some(bridge_query_planner) = js_planners.first().cloned() { Self::get_v8_metrics( bridge_query_planner, v8_heap_used.clone(), @@ -150,21 +149,33 @@ impl BridgeQueryPlannerPool { } Ok(Self { - js_planners: planners, + js_planners, sender, schema, subgraph_schemas, - _pool_size_gauge: pool_size_gauge, + pool_size_gauge: Default::default(), v8_heap_used, - _v8_heap_used_gauge, + v8_heap_used_gauge: Default::default(), v8_heap_total, - _v8_heap_total_gauge, + v8_heap_total_gauge: Default::default(), + introspection_cache, }) } - fn create_heap_used_gauge(meter: &Meter) -> (Arc, ObservableGauge) { - let current_heap_used = Arc::new(AtomicU64::new(0)); - let current_heap_used_for_gauge = current_heap_used.clone(); + fn create_pool_size_gauge(&self) -> ObservableGauge { + let sender = self.sender.clone(); + let meter = meter_provider().meter("apollo/router"); + meter + .u64_observable_gauge("apollo.router.query_planning.queued") + .with_description("Number of queries waiting to be planned") + .with_unit(Unit::new("query")) + .with_callback(move |m| m.observe(sender.len() as u64, &[])) + .init() + } + + fn create_heap_used_gauge(&self) -> ObservableGauge { + let meter = meter_provider().meter("apollo/router"); + let current_heap_used_for_gauge = self.v8_heap_used.clone(); let heap_used_gauge = meter .u64_observable_gauge("apollo.router.v8.heap.used") .with_description("V8 heap used, in bytes") @@ -173,12 +184,12 @@ impl BridgeQueryPlannerPool { i.observe(current_heap_used_for_gauge.load(Ordering::SeqCst), &[]) }) .init(); - (current_heap_used, heap_used_gauge) + heap_used_gauge } - fn create_heap_total_gauge(meter: &Meter) -> (Arc, ObservableGauge) { - let current_heap_total = Arc::new(AtomicU64::new(0)); - let current_heap_total_for_gauge = current_heap_total.clone(); + fn create_heap_total_gauge(&self) -> ObservableGauge { + let meter = meter_provider().meter("apollo/router"); + let current_heap_total_for_gauge = self.v8_heap_total.clone(); let heap_total_gauge = meter .u64_observable_gauge("apollo.router.v8.heap.total") .with_description("V8 heap total, in bytes") @@ -187,10 +198,10 @@ impl BridgeQueryPlannerPool { i.observe(current_heap_total_for_gauge.load(Ordering::SeqCst), &[]) }) .init(); - (current_heap_total, heap_total_gauge) + heap_total_gauge } - pub(crate) fn planners(&self) -> Vec>> { + pub(crate) fn js_planners(&self) -> Vec>> { self.js_planners.clone() } @@ -215,6 +226,17 @@ impl BridgeQueryPlannerPool { v8_heap_total.store(metrics.heap_total, Ordering::SeqCst); } } + + pub(super) fn activate(&self) { + // Gauges MUST be initialized after a meter provider is created. + // When a hot reload happens this means that the gauges must be re-initialized. + *self.pool_size_gauge.lock().expect("lock poisoned") = Some(self.create_pool_size_gauge()); + *self.v8_heap_used_gauge.lock().expect("lock poisoned") = + Some(self.create_heap_used_gauge()); + *self.v8_heap_total_gauge.lock().expect("lock poisoned") = + Some(self.create_heap_total_gauge()); + self.introspection_cache.activate(); + } } impl tower::Service for BridgeQueryPlannerPool { @@ -243,13 +265,10 @@ impl tower::Service for BridgeQueryPlannerPool { let get_metrics_future = if let Some(bridge_query_planner) = self.js_planners.first().cloned() { - let v8_heap_used = self.v8_heap_used.clone(); - let v8_heap_total = self.v8_heap_total.clone(); - Some(Self::get_v8_metrics( bridge_query_planner, - v8_heap_used, - v8_heap_total, + self.v8_heap_used.clone(), + self.v8_heap_total.clone(), )) } else { None @@ -297,12 +316,14 @@ mod tests { async move { let mut pool = BridgeQueryPlannerPool::new( + Vec::new(), schema.clone(), config.clone(), NonZeroUsize::new(2).unwrap(), ) .await .unwrap(); + pool.activate(); let query = "query { me { name } }".to_string(); let doc = Query::parse_document(&query, None, &schema, &config).unwrap(); diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index f6270381cb..b0a527584e 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -28,6 +28,7 @@ use crate::cache::estimate_size; use crate::cache::storage::InMemoryCache; use crate::cache::storage::ValueType; use crate::cache::DeduplicatingCache; +use crate::configuration::PersistedQueriesPrewarmQueryPlanCache; use crate::error::CacheResolverError; use crate::error::QueryPlannerError; use crate::plugins::authorization::AuthorizationPlugin; @@ -167,7 +168,7 @@ where previous_cache: Option, count: Option, experimental_reuse_query_plans: bool, - experimental_pql_prewarm: bool, + experimental_pql_prewarm: &PersistedQueriesPrewarmQueryPlanCache, ) { let _timer = Timer::new(|duration| { ::tracing::info!( @@ -223,8 +224,9 @@ where cache_keys.shuffle(&mut thread_rng()); - let should_warm_with_pqs = - (experimental_pql_prewarm && previous_cache.is_none()) || previous_cache.is_some(); + let should_warm_with_pqs = (experimental_pql_prewarm.on_startup + && previous_cache.is_none()) + || (experimental_pql_prewarm.on_reload && previous_cache.is_some()); let persisted_queries_operations = persisted_query_layer.all_operations(); let capacity = if should_warm_with_pqs { @@ -382,8 +384,8 @@ where } impl CachingQueryPlanner { - pub(crate) fn planners(&self) -> Vec>> { - self.delegate.planners() + pub(crate) fn js_planners(&self) -> Vec>> { + self.delegate.js_planners() } pub(crate) fn subgraph_schemas( @@ -391,6 +393,11 @@ impl CachingQueryPlanner { ) -> Arc>>> { self.delegate.subgraph_schemas() } + + pub(crate) fn activate(&self) { + self.cache.activate(); + self.delegate.activate(); + } } impl tower::Service diff --git a/apollo-router/src/query_planner/convert.rs b/apollo-router/src/query_planner/convert.rs index bf59d1861e..27592834c6 100644 --- a/apollo-router/src/query_planner/convert.rs +++ b/apollo-router/src/query_planner/convert.rs @@ -306,10 +306,23 @@ impl From<&'_ next::FetchDataKeyRenamer> for rewrites::DataKeyRenamer { impl From<&'_ next::FetchDataPathElement> for crate::json_ext::PathElement { fn from(value: &'_ next::FetchDataPathElement) -> Self { + // TODO: Go all in on Name eventually match value { - // TODO: Type conditioned fetching once it's available in the rust planner - next::FetchDataPathElement::Key(value) => Self::Key(value.to_string(), None), - next::FetchDataPathElement::AnyIndex => Self::Flatten(None), + next::FetchDataPathElement::Key(name, conditions) => Self::Key( + name.to_string(), + if conditions.is_empty() { + None + } else { + Some(conditions.iter().map(|c| c.to_string()).collect()) + }, + ), + next::FetchDataPathElement::AnyIndex(conditions) => { + Self::Flatten(if conditions.is_empty() { + None + } else { + Some(conditions.iter().map(|c| c.to_string()).collect()) + }) + } next::FetchDataPathElement::TypenameEquals(value) => Self::Fragment(value.to_string()), } } diff --git a/apollo-router/src/query_planner/dual_introspection.rs b/apollo-router/src/query_planner/dual_introspection.rs new file mode 100644 index 0000000000..a6c51d7f63 --- /dev/null +++ b/apollo-router/src/query_planner/dual_introspection.rs @@ -0,0 +1,137 @@ +use std::cmp::Ordering; + +use serde_json_bytes::Value; + +use crate::error::QueryPlannerError; +use crate::graphql; + +pub(crate) fn compare_introspection_responses( + query: &str, + mut js_result: Result, + mut rust_result: Result, +) { + let is_matched; + match (&mut js_result, &mut rust_result) { + (Err(_), Err(_)) => { + is_matched = true; + } + (Err(err), Ok(_)) => { + is_matched = false; + tracing::warn!("JS introspection error: {err}") + } + (Ok(_), Err(err)) => { + is_matched = false; + tracing::warn!("Rust introspection error: {err}") + } + (Ok(js_response), Ok(rust_response)) => { + if let (Some(js_data), Some(rust_data)) = + (&mut js_response.data, &mut rust_response.data) + { + json_sort_arrays(js_data); + json_sort_arrays(rust_data); + } + is_matched = js_response.data == rust_response.data; + if is_matched { + tracing::debug!("Introspection match! 🎉") + } else { + tracing::debug!("Introspection mismatch"); + tracing::trace!("Introspection query:\n{query}"); + tracing::trace!("Introspection diff:\n{}", { + let rust = rust_response + .data + .as_ref() + .map(|d| serde_json::to_string_pretty(&d).unwrap()) + .unwrap_or_default(); + let js = js_response + .data + .as_ref() + .map(|d| serde_json::to_string_pretty(&d).unwrap()) + .unwrap_or_default(); + let diff = similar::TextDiff::from_lines(&js, &rust); + diff.unified_diff() + .context_radius(10) + .header("JS", "Rust") + .to_string() + }) + } + } + } + + u64_counter!( + "apollo.router.operations.introspection.both", + "Comparing JS v.s. Rust introspection", + 1, + "generation.is_matched" = is_matched, + "generation.js_error" = js_result.is_err(), + "generation.rust_error" = rust_result.is_err() + ); +} + +fn json_sort_arrays(value: &mut Value) { + match value { + Value::Array(array) => { + for item in array.iter_mut() { + json_sort_arrays(item) + } + array.sort_by(json_compare) + } + Value::Object(object) => { + for (_key, value) in object { + json_sort_arrays(value) + } + } + Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {} + } +} + +fn json_compare(a: &Value, b: &Value) -> Ordering { + match (a, b) { + (Value::Null, Value::Null) => Ordering::Equal, + (Value::Bool(a), Value::Bool(b)) => a.cmp(b), + (Value::Number(a), Value::Number(b)) => a.as_f64().unwrap().total_cmp(&b.as_f64().unwrap()), + (Value::String(a), Value::String(b)) => a.cmp(b), + (Value::Array(a), Value::Array(b)) => iter_cmp(a, b, json_compare), + (Value::Object(a), Value::Object(b)) => { + iter_cmp(a, b, |(key_a, a), (key_b, b)| { + debug_assert_eq!(key_a, key_b); // Response object keys are in selection set order + json_compare(a, b) + }) + } + _ => json_discriminant(a).cmp(&json_discriminant(b)), + } +} + +// TODO: use `Iterator::cmp_by` when available: +// https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.cmp_by +// https://github.com/rust-lang/rust/issues/64295 +fn iter_cmp( + a: impl IntoIterator, + b: impl IntoIterator, + cmp: impl Fn(T, T) -> Ordering, +) -> Ordering { + use itertools::Itertools; + for either_or_both in a.into_iter().zip_longest(b) { + match either_or_both { + itertools::EitherOrBoth::Both(a, b) => { + let ordering = cmp(a, b); + if ordering != Ordering::Equal { + return ordering; + } + } + itertools::EitherOrBoth::Left(_) => return Ordering::Less, + itertools::EitherOrBoth::Right(_) => return Ordering::Greater, + } + } + Ordering::Equal +} + +fn json_discriminant(value: &Value) -> u8 { + match value { + Value::Null => 0, + Value::Bool(_) => 1, + Value::Number(_) => 2, + Value::String(_) => 3, + Value::Array(_) => 4, + Value::Object(_) => 5, + } +} diff --git a/apollo-router/src/query_planner/dual_query_planner.rs b/apollo-router/src/query_planner/dual_query_planner.rs index 6a880cf538..9952eb9782 100644 --- a/apollo-router/src/query_planner/dual_query_planner.rs +++ b/apollo-router/src/query_planner/dual_query_planner.rs @@ -12,6 +12,7 @@ use apollo_compiler::ast; use apollo_compiler::validation::Valid; use apollo_compiler::ExecutableDocument; use apollo_compiler::Name; +use apollo_federation::query_plan::query_planner::QueryPlanOptions; use apollo_federation::query_plan::query_planner::QueryPlanner; use apollo_federation::query_plan::QueryPlan; @@ -22,6 +23,7 @@ use super::FlattenNode; use crate::error::format_bridge_errors; use crate::executable::USING_CATCH_UNWIND; use crate::query_planner::bridge_query_planner::metric_query_planning_plan_duration; +use crate::query_planner::bridge_query_planner::JS_QP_MODE; use crate::query_planner::bridge_query_planner::RUST_QP_MODE; use crate::query_planner::convert::convert_root_query_plan_node; use crate::query_planner::render_diff; @@ -38,9 +40,11 @@ const WORKER_THREAD_COUNT: usize = 1; pub(crate) struct BothModeComparisonJob { pub(crate) rust_planner: Arc, + pub(crate) js_duration: f64, pub(crate) document: Arc>, pub(crate) operation_name: Option, pub(crate) js_result: Result>>, + pub(crate) plan_options: QueryPlanOptions, } type Queue = crossbeam_channel::Sender; @@ -86,9 +90,15 @@ impl BothModeComparisonJob { let start = Instant::now(); // No question mark operator or macro from here … - let result = self.rust_planner.build_query_plan(&self.document, name); + let result = + self.rust_planner + .build_query_plan(&self.document, name, self.plan_options); - metric_query_planning_plan_duration(RUST_QP_MODE, start); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); + + metric_query_planning_plan_both_comparison_duration(RUST_QP_MODE, elapsed); + metric_query_planning_plan_both_comparison_duration(JS_QP_MODE, self.js_duration); // … to here, so the thread can only eiher reach here or panic. // We unset USING_CATCH_UNWIND in both cases. @@ -169,6 +179,18 @@ impl BothModeComparisonJob { } } +pub(crate) fn metric_query_planning_plan_both_comparison_duration( + planner: &'static str, + elapsed: f64, +) { + f64_histogram!( + "apollo.router.operations.query_planner.both.duration", + "Comparing JS v.s. Rust query plan duration.", + elapsed, + "planner" = planner + ); +} + // Specific comparison functions pub struct MatchFailure { @@ -819,3 +841,31 @@ mod ast_comparison_tests { assert!(super::same_ast_document(&ast_x, &ast_y).is_ok()); } } + +#[cfg(test)] +mod tests { + use std::time::Instant; + + use super::*; + + #[test] + fn test_metric_query_planning_plan_both_comparison_duration() { + let start = Instant::now(); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_both_comparison_duration(RUST_QP_MODE, elapsed); + assert_histogram_exists!( + "apollo.router.operations.query_planner.both.duration", + f64, + "planner" = "rust" + ); + + let start = Instant::now(); + let elapsed = start.elapsed().as_secs_f64(); + metric_query_planning_plan_both_comparison_duration(JS_QP_MODE, elapsed); + assert_histogram_exists!( + "apollo.router.operations.query_planner.both.duration", + f64, + "planner" = "js" + ); + } +} diff --git a/apollo-router/src/query_planner/labeler.rs b/apollo-router/src/query_planner/labeler.rs index a993e1dbd3..856dc7385c 100644 --- a/apollo-router/src/query_planner/labeler.rs +++ b/apollo-router/src/query_planner/labeler.rs @@ -11,6 +11,7 @@ use tower::BoxError; use crate::spec::query::subselections::DEFER_DIRECTIVE_NAME; use crate::spec::query::transform; use crate::spec::query::transform::document; +use crate::spec::query::transform::TransformState; use crate::spec::query::transform::Visitor; const LABEL_NAME: Name = name!("label"); @@ -25,12 +26,14 @@ pub(crate) fn add_defer_labels( let mut visitor = Labeler { next_label: 0, schema, + state: TransformState::new(), }; document(&mut visitor, doc) } pub(crate) struct Labeler<'a> { schema: &'a Schema, + state: TransformState, next_label: u32, } @@ -65,6 +68,10 @@ impl Visitor for Labeler<'_> { fn schema(&self) -> &apollo_compiler::Schema { self.schema } + + fn state(&mut self) -> &mut TransformState { + &mut self.state + } } fn directives( diff --git a/apollo-router/src/query_planner/mod.rs b/apollo-router/src/query_planner/mod.rs index ad23fbb80e..bff26390e9 100644 --- a/apollo-router/src/query_planner/mod.rs +++ b/apollo-router/src/query_planner/mod.rs @@ -14,6 +14,7 @@ pub(crate) mod bridge_query_planner; mod bridge_query_planner_pool; mod caching_query_planner; mod convert; +mod dual_introspection; pub(crate) mod dual_query_planner; mod execution; pub(crate) mod fetch; diff --git a/apollo-router/src/query_planner/rewrites.rs b/apollo-router/src/query_planner/rewrites.rs index 49733e9ad1..878c18da04 100644 --- a/apollo-router/src/query_planner/rewrites.rs +++ b/apollo-router/src/query_planner/rewrites.rs @@ -116,36 +116,7 @@ mod tests { // The schema is not used for the tests // but we need a valid one - const SCHEMA: &str = r#" - schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") - { - query: Query - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - - enum join__Graph { - FAKE @join__graph(name:"fake" url: "http://localhost:4001/fake") - } - - type Query { - i: [I] - } - - interface I { - x: Int - } - - type A implements I { - x: Int - } - - type B { - y: Int - } - "#; + const SCHEMA: &str = include_str!("../testdata/minimal_supergraph.graphql"); #[test] fn test_key_renamer_object() { diff --git a/apollo-router/src/query_planner/selection.rs b/apollo-router/src/query_planner/selection.rs index e2c5e9b013..810c1c1ac5 100644 --- a/apollo-router/src/query_planner/selection.rs +++ b/apollo-router/src/query_planner/selection.rs @@ -367,7 +367,7 @@ mod tests { assert_eq!( select!( with_supergraph_boilerplate( - "type Query { me: String } type Author { name: String } type Reviewer { name: String } \ + "type Query @join__type(graph: TEST) { me: String @join__field(graph: TEST) } type Author { name: String } type Reviewer { name: String } \ union User = Author | Reviewer" ), bjson!({"__typename": "Author", "id":2, "name":"Bob", "job":{"name":"astronaut"}}), @@ -400,7 +400,7 @@ mod tests { #[test] fn test_array() { let schema = with_supergraph_boilerplate( - "type Query { me: String } + "type Query @join__type(graph: TEST){ me: String @join__field(graph: TEST) } type MainObject { mainObjectList: [SubObject] } type SubObject { key: String name: String }", ); @@ -471,7 +471,7 @@ mod tests { #[test] fn test_execute_selection_set_abstract_types() { let schema = with_supergraph_boilerplate( - "type Query { hello: String } + "type Query @join__type(graph: TEST){ hello: String @join__field(graph: TEST)} type Entity { id: Int! nestedUnion: NestedUnion @@ -743,16 +743,62 @@ mod tests { "{}\n{}", r#" schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") { - query: Query + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query } - directive @core(feature: String!) repeatable on SCHEMA + + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + + directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean + ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + directive @join__graph(name: String!, url: String!) on ENUM_VALUE + + directive @join__implements( + graph: join__Graph! + interface: String! + ) repeatable on OBJECT | INTERFACE + + directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false + ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + + directive @join__unionMember( + graph: join__Graph! + member: String! + ) repeatable on UNION + + directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] + ) repeatable on SCHEMA + + scalar join__FieldSet + enum join__Graph { TEST @join__graph(name: "test", url: "http://localhost:4001/graphql") } - + + scalar link__Import + + enum link__Purpose { + SECURITY + EXECUTION + } "#, content ) diff --git a/apollo-router/src/query_planner/testdata/defer_schema.graphql b/apollo-router/src/query_planner/testdata/defer_schema.graphql index 54c0b4db9b..75dc482375 100644 --- a/apollo-router/src/query_planner/testdata/defer_schema.graphql +++ b/apollo-router/src/query_planner/testdata/defer_schema.graphql @@ -1,39 +1,72 @@ schema -@core(feature: "https://specs.apollo.dev/core/v0.1"), -@core(feature: "https://specs.apollo.dev/join/v0.1") -{ - query: Query + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR -directive @stream on FIELD +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA directive @transform(from: String!) on FIELD +directive @stream on FIELD + scalar join__FieldSet enum join__Graph { -X @join__graph(name: "X" url: "http://X") -Y @join__graph(name: "Y" url: "http://Y") + X @join__graph(name: "X", url: "http://X") + Y @join__graph(name: "Y", url: "http://Y") } -type Query { - t: T @join__field(graph: X) +scalar link__Import + +enum link__Purpose { + SECURITY + EXECUTION } -type T -@join__owner(graph: X) -@join__type(graph: X, key: "id") { - id: ID @join__field(graph: X) - x: String @join__field(graph: X) - y: String @join__field(graph: Y) -} \ No newline at end of file +type Query @join__type(graph: X) @join__type(graph: Y) { + t: T @join__field(graph: X) +} + +type T @join__type(graph: X, key: "id") { + id: ID @join__field(graph: X) + x: String @join__field(graph: X) + y: String @join__field(graph: Y) +} diff --git a/apollo-router/src/query_planner/testdata/schema.graphql b/apollo-router/src/query_planner/testdata/schema.graphql index f019d0dd3f..ce450759f8 100644 --- a/apollo-router/src/query_planner/testdata/schema.graphql +++ b/apollo-router/src/query_planner/testdata/schema.graphql @@ -1,272 +1,341 @@ schema -@core(feature: "https://specs.apollo.dev/core/v0.1"), -@core(feature: "https://specs.apollo.dev/join/v0.1") -{ - query: Query - mutation: Mutation + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query + mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +scalar join__FieldSet + +enum join__Graph { + ACCOUNTS @join__graph(name: "accounts", url: "http://accounts") + BOOKS @join__graph(name: "books", url: "http://books") + DOCUMENTS @join__graph(name: "documents", url: "http://documents") + INVENTORY @join__graph(name: "inventory", url: "http://inventory") + PRODUCT @join__graph(name: "product", url: "http://products") + REVIEWS @join__graph(name: "reviews", url: "http://reviews") +} + +scalar link__Import +enum link__Purpose { + SECURITY + EXECUTION +} directive @stream on FIELD directive @transform(from: String!) on FIELD -union AccountType = PasswordAccount | SMSAccount +union AccountType @join__type(graph: ACCOUNTS) = PasswordAccount | SMSAccount -type Amazon { -referrer: String +type Amazon @join__type(graph: PRODUCT, key: "referrer") { + referrer: String } -union Body = Image | Text +union Body @join__type(graph: DOCUMENTS) = Image | Text type Book implements Product -@join__owner(graph: BOOKS) -@join__type(graph: BOOKS, key: "isbn") -@join__type(graph: INVENTORY, key: "isbn") -@join__type(graph: PRODUCT, key: "isbn") -@join__type(graph: REVIEWS, key: "isbn") -{ -isbn: String! @join__field(graph: BOOKS) -title: String @join__field(graph: BOOKS) -year: Int @join__field(graph: BOOKS) -similarBooks: [Book]! @join__field(graph: BOOKS) -metadata: [MetadataOrError] @join__field(graph: BOOKS) -inStock: Boolean @join__field(graph: INVENTORY) -isCheckedOut: Boolean @join__field(graph: INVENTORY) -upc: String! @join__field(graph: PRODUCT) -sku: String! @join__field(graph: PRODUCT) -name(delimeter: String = " "): String @join__field(graph: PRODUCT, requires: "title year") -price: String @join__field(graph: PRODUCT) -details: ProductDetailsBook @join__field(graph: PRODUCT) -reviews: [Review] @join__field(graph: REVIEWS) -relatedReviews: [Review!]! @join__field(graph: REVIEWS, requires: "similarBooks{isbn}") + @join__type(graph: BOOKS, key: "isbn") + @join__type(graph: INVENTORY, key: "isbn") + @join__type(graph: PRODUCT, key: "isbn") + @join__type(graph: REVIEWS, key: "isbn") { + isbn: String! + @join__field(graph: BOOKS) + @join__field(graph: REVIEWS, external: true) + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCT, external: true) + title: String + @join__field(graph: BOOKS) + @join__field(graph: PRODUCT, external: true) + year: Int + @join__field(graph: BOOKS) + @join__field(graph: PRODUCT, external: true) + similarBooks: [Book]! + @join__field(graph: BOOKS) + @join__field(graph: REVIEWS, external: true) + metadata: [MetadataOrError] @join__field(graph: BOOKS) + inStock: Boolean @join__field(graph: INVENTORY) + isCheckedOut: Boolean @join__field(graph: INVENTORY) + upc: String! @join__field(graph: PRODUCT) + sku: String! @join__field(graph: PRODUCT) + name(delimeter: String = " "): String + @join__field(graph: PRODUCT, requires: "title year") + price: String @join__field(graph: PRODUCT) + details: ProductDetailsBook @join__field(graph: PRODUCT) + reviews: [Review] @join__field(graph: REVIEWS) + relatedReviews: [Review!]! + @join__field(graph: REVIEWS, requires: "similarBooks{isbn}") } -union Brand = Ikea | Amazon +union Brand @join__type(graph: PRODUCT) = Ikea | Amazon type Car implements Vehicle -@join__owner(graph: PRODUCT) -@join__type(graph: PRODUCT, key: "id") -@join__type(graph: REVIEWS, key: "id") -{ -id: String! @join__field(graph: PRODUCT) -description: String @join__field(graph: PRODUCT) -price: String @join__field(graph: PRODUCT) -retailPrice: String @join__field(graph: REVIEWS, requires: "price") + @join__type(graph: PRODUCT, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: String! @join__field(graph: PRODUCT) @join__field(graph: REVIEWS) + description: String @join__field(graph: PRODUCT) + price: String + @join__field(graph: PRODUCT) + @join__field(graph: REVIEWS, external: true) + retailPrice: String @join__field(graph: REVIEWS, requires: "price") } -type Error { -code: Int -message: String +type Error + @join__type(graph: REVIEWS, key: "code") + @join__type(graph: PRODUCT, key: "code") + @join__type(graph: BOOKS, key: "code") { + code: Int + message: String } type Furniture implements Product -@join__owner(graph: PRODUCT) -@join__type(graph: PRODUCT, key: "upc") -@join__type(graph: PRODUCT, key: "sku") -@join__type(graph: INVENTORY, key: "sku") -@join__type(graph: REVIEWS, key: "upc") -{ -upc: String! @join__field(graph: PRODUCT) -sku: String! @join__field(graph: PRODUCT) -name: String @join__field(graph: PRODUCT) -price: String @join__field(graph: PRODUCT) -brand: Brand @join__field(graph: PRODUCT) -metadata: [MetadataOrError] @join__field(graph: PRODUCT) -details: ProductDetailsFurniture @join__field(graph: PRODUCT) -inStock: Boolean @join__field(graph: INVENTORY) -isHeavy: Boolean @join__field(graph: INVENTORY) -reviews: [Review] @join__field(graph: REVIEWS) + @join__type(graph: PRODUCT, key: "upc") + @join__type(graph: PRODUCT, key: "sku") + @join__type(graph: INVENTORY, key: "sku") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + @join__field(graph: PRODUCT) + @join__field(graph: REVIEWS, external: true) + sku: String! + @join__field(graph: PRODUCT) + @join__field(graph: INVENTORY, external: true) + name: String @join__field(graph: PRODUCT) + price: String @join__field(graph: PRODUCT) + brand: Brand @join__field(graph: PRODUCT) + metadata: [MetadataOrError] @join__field(graph: PRODUCT) + details: ProductDetailsFurniture @join__field(graph: PRODUCT) + inStock: Boolean @join__field(graph: INVENTORY) + isHeavy: Boolean @join__field(graph: INVENTORY) + reviews: [Review] @join__field(graph: REVIEWS) } -type Ikea { -asile: Int +type Ikea @join__type(graph: PRODUCT, key: "asile") { + asile: Int } -type Image implements NamedObject { -name: String! -attributes: ImageAttributes! +type Image implements NamedObject @join__type(graph: DOCUMENTS, key: "name") { + name: String! + attributes: ImageAttributes! } -type ImageAttributes { -url: String! +type ImageAttributes @join__type(graph: DOCUMENTS, key: "url") { + url: String! } -scalar join__FieldSet - -enum join__Graph { -ACCOUNTS @join__graph(name: "accounts" url: "http://accounts") -BOOKS @join__graph(name: "books" url: "http://books") -DOCUMENTS @join__graph(name: "documents" url: "http://documents") -INVENTORY @join__graph(name: "inventory" url: "http://inventory") -PRODUCT @join__graph(name: "product" url: "http://products") -REVIEWS @join__graph(name: "reviews" url: "http://reviews") -} - -type KeyValue { -key: String! -value: String! +type KeyValue + @join__type(graph: REVIEWS, key: "key") + @join__type(graph: PRODUCT, key: "key") + @join__type(graph: BOOKS, key: "key") { + key: String! + value: String! } type Library -@join__owner(graph: BOOKS) -@join__type(graph: BOOKS, key: "id") -@join__type(graph: ACCOUNTS, key: "id") -{ -id: ID! @join__field(graph: BOOKS) -name: String @join__field(graph: BOOKS) -userAccount(id: ID! = 1): User @join__field(graph: ACCOUNTS, requires: "name") + @join__type(graph: BOOKS, key: "id") + @join__type(graph: ACCOUNTS, key: "id") { + id: ID! + name: String + @join__field(graph: BOOKS) + @join__field(graph: ACCOUNTS, external: true) + userAccount(id: ID! = 1): User @join__field(graph: ACCOUNTS, requires: "name") } -union MetadataOrError = KeyValue | Error - -type Mutation { -login(username: String!, password: String!): User @join__field(graph: ACCOUNTS) -reviewProduct(upc: String!, body: String!): Product @join__field(graph: REVIEWS) -updateReview(review: UpdateReviewInput!): Review @join__field(graph: REVIEWS) -deleteReview(id: ID!): Boolean @join__field(graph: REVIEWS) +union MetadataOrError + @join__type(graph: REVIEWS) + @join__type(graph: PRODUCT) + @join__type(graph: BOOKS) + @join__type(graph: REVIEWS) + @join__type(graph: PRODUCT) + @join__type(graph: BOOKS) = + KeyValue + | Error + +type Mutation @join__type(graph: ACCOUNTS) @join__type(graph: REVIEWS) { + login(username: String!, password: String!): User + @join__field(graph: ACCOUNTS) + reviewProduct(upc: String!, body: String!): Product + @join__field(graph: REVIEWS) + updateReview(review: UpdateReviewInput!): Review @join__field(graph: REVIEWS) + deleteReview(id: ID!): Boolean @join__field(graph: REVIEWS) } -type Name { -first: String -last: String +type Name @join__type(graph: ACCOUNTS) { + first: String + last: String } -interface NamedObject { -name: String! +interface NamedObject @join__type(graph: DOCUMENTS) { + name: String! } -type PasswordAccount -@join__owner(graph: ACCOUNTS) -@join__type(graph: ACCOUNTS, key: "email") -{ -email: String! @join__field(graph: ACCOUNTS) +type PasswordAccount @join__type(graph: ACCOUNTS, key: "email") { + email: String! @join__field(graph: ACCOUNTS) } -interface Product { -upc: String! -sku: String! -name: String -price: String -details: ProductDetails -inStock: Boolean -reviews: [Review] +interface Product @join__type(graph: PRODUCT) @join__type(graph: REVIEWS) { + upc: String! + sku: String! + name: String + price: String + details: ProductDetails + inStock: Boolean + reviews: [Review] } -interface ProductDetails { -country: String +interface ProductDetails + @join__type(graph: PRODUCT) + @join__type(graph: REVIEWS) { + country: String } -type ProductDetailsBook implements ProductDetails { -country: String -pages: Int +type ProductDetailsBook implements ProductDetails @join__type(graph: PRODUCT) { + country: String + pages: Int } -type ProductDetailsFurniture implements ProductDetails { -country: String -color: String +type ProductDetailsFurniture implements ProductDetails + @join__type(graph: PRODUCT) { + country: String + color: String } -type Query { -user(id: ID!): User @join__field(graph: ACCOUNTS) -me: User @join__field(graph: ACCOUNTS) -book(isbn: String!): Book @join__field(graph: BOOKS) -books: [Book] @join__field(graph: BOOKS) -library(id: ID!): Library @join__field(graph: BOOKS) -body: Body! @join__field(graph: DOCUMENTS) -product(upc: String!): Product @join__field(graph: PRODUCT) -vehicle(id: String!): Vehicle @join__field(graph: PRODUCT) -topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCT) -topCars(first: Int = 5): [Car] @join__field(graph: PRODUCT) -topReviews(first: Int = 5): [Review] @join__field(graph: REVIEWS) +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: DOCUMENTS) + @join__type(graph: BOOKS) + @join__type(graph: PRODUCT) + @join__type(graph: REVIEWS) { + user(id: ID!): User @join__field(graph: ACCOUNTS) + me: User @join__field(graph: ACCOUNTS) + book(isbn: String!): Book @join__field(graph: BOOKS) + books: [Book] @join__field(graph: BOOKS) + library(id: ID!): Library @join__field(graph: BOOKS) + body: Body! @join__field(graph: DOCUMENTS) + product(upc: String!): Product @join__field(graph: PRODUCT) + vehicle(id: String!): Vehicle @join__field(graph: PRODUCT) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCT) + topCars(first: Int = 5): [Car] @join__field(graph: PRODUCT) + topReviews(first: Int = 5): [Review] @join__field(graph: REVIEWS) } type Review -@join__owner(graph: REVIEWS) -@join__type(graph: REVIEWS, key: "id") -{ -id: ID! @join__field(graph: REVIEWS) -body(format: Boolean = false): String @join__field(graph: REVIEWS) -author: User @join__field(graph: REVIEWS, provides: "username") -product: Product @join__field(graph: REVIEWS) -metadata: [MetadataOrError] @join__field(graph: REVIEWS) + @join__type(graph: REVIEWS, key: "id") + @join__type(graph: PRODUCT, key: "id") { + id: ID! @join__field(graph: REVIEWS) + body(format: Boolean = false): String @join__field(graph: REVIEWS) + author: User @join__field(graph: REVIEWS, provides: "username") + product: Product @join__field(graph: REVIEWS) + metadata: [MetadataOrError] @join__field(graph: REVIEWS) } -type SMSAccount -@join__owner(graph: ACCOUNTS) -@join__type(graph: ACCOUNTS, key: "number") -{ -number: String @join__field(graph: ACCOUNTS) +type SMSAccount @join__type(graph: ACCOUNTS, key: "number") { + number: String @join__field(graph: ACCOUNTS) } -type Text implements NamedObject { -name: String! -attributes: TextAttributes! +type Text implements NamedObject @join__type(graph: DOCUMENTS, key: "name") { + name: String! + attributes: TextAttributes! } -type TextAttributes { -bold: Boolean -text: String +type TextAttributes @join__type(graph: DOCUMENTS) { + bold: Boolean + text: String } -union Thing = Car | Ikea +union Thing @join__type(graph: PRODUCT) = Car | Ikea -input UpdateReviewInput { -id: ID! -body: String +input UpdateReviewInput @join__type(graph: REVIEWS) { + id: ID! + body: String } type User -@join__owner(graph: ACCOUNTS) -@join__type(graph: ACCOUNTS, key: "id") -@join__type(graph: ACCOUNTS, key: "username name{first last}") -@join__type(graph: INVENTORY, key: "id") -@join__type(graph: PRODUCT, key: "id") -@join__type(graph: REVIEWS, key: "id") -{ -id: ID! @join__field(graph: ACCOUNTS) -name: Name @join__field(graph: ACCOUNTS) -username: String @join__field(graph: ACCOUNTS) -birthDate(locale: String): String @join__field(graph: ACCOUNTS) -account: AccountType @join__field(graph: ACCOUNTS) -metadata: [UserMetadata] @join__field(graph: ACCOUNTS) -goodDescription: Boolean @join__field(graph: INVENTORY, requires: "metadata{description}") -vehicle: Vehicle @join__field(graph: PRODUCT) -thing: Thing @join__field(graph: PRODUCT) -reviews: [Review] @join__field(graph: REVIEWS) -numberOfReviews: Int! @join__field(graph: REVIEWS) -goodAddress: Boolean @join__field(graph: REVIEWS, requires: "metadata{address}") + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: ACCOUNTS, key: "username name{first last}") + @join__type(graph: INVENTORY, key: "id") + @join__type(graph: PRODUCT, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! + name: Name @join__field(graph: ACCOUNTS) + username: String @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS) + birthDate(locale: String): String @join__field(graph: ACCOUNTS) + account: AccountType @join__field(graph: ACCOUNTS) + metadata: [UserMetadata] + @join__field(graph: ACCOUNTS) + @join__field(graph: INVENTORY, external: true) + @join__field(graph: REVIEWS, external: true) + goodDescription: Boolean + @join__field(graph: INVENTORY, requires: "metadata{description}") + vehicle: Vehicle @join__field(graph: PRODUCT) + thing: Thing @join__field(graph: PRODUCT) + reviews: [Review] @join__field(graph: REVIEWS) + numberOfReviews: Int! @join__field(graph: REVIEWS) + goodAddress: Boolean + @join__field(graph: REVIEWS, requires: "metadata{address}") } -type UserMetadata { -name: String -address: String -description: String +type UserMetadata + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: REVIEWS) { + name: String + address: String + description: String } type Van implements Vehicle -@join__owner(graph: PRODUCT) -@join__type(graph: PRODUCT, key: "id") -@join__type(graph: REVIEWS, key: "id") -{ -id: String! @join__field(graph: PRODUCT) -description: String @join__field(graph: PRODUCT) -price: String @join__field(graph: PRODUCT) -retailPrice: String @join__field(graph: REVIEWS, requires: "price") + @join__type(graph: PRODUCT, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: String! + description: String @join__field(graph: PRODUCT) + price: String + @join__field(graph: PRODUCT) + @join__field(graph: REVIEWS, external: true) + retailPrice: String @join__field(graph: REVIEWS, requires: "price") } -interface Vehicle { -id: String! -description: String -price: String -retailPrice: String +interface Vehicle @join__type(graph: PRODUCT) { + id: String! + description: String + price: String + retailPrice: String } diff --git a/apollo-router/src/query_planner/tests.rs b/apollo-router/src/query_planner/tests.rs index cfd44d6d08..131dc7ba12 100644 --- a/apollo-router/src/query_planner/tests.rs +++ b/apollo-router/src/query_planner/tests.rs @@ -575,34 +575,7 @@ async fn defer_if_condition() { #[tokio::test] async fn dependent_mutations() { - let schema = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") - { - query: Query - mutation: Mutation - } - - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - scalar join__FieldSet - - enum join__Graph { - A @join__graph(name: "A" url: "http://localhost:4001") - B @join__graph(name: "B" url: "http://localhost:4004") - } - - type Mutation { - mutationA: Mutation @join__field(graph: A) - mutationB: Boolean @join__field(graph: B) - } - - type Query { - query: Boolean @join__field(graph: A) - }"#; + let schema = include_str!("../testdata/a_b_supergraph.graphql"); let query_plan: QueryPlan = QueryPlan { // generated from: diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs index 4a5ce3f888..0cf4bda19d 100644 --- a/apollo-router/src/router_factory.rs +++ b/apollo-router/src/router_factory.rs @@ -253,7 +253,7 @@ impl YamlRouterFactory { .supergraph .query_planning .experimental_reuse_query_plans, - configuration + &configuration .persisted_queries .experimental_prewarm_query_plan_cache, ) @@ -269,7 +269,7 @@ impl YamlRouterFactory { .supergraph .query_planning .experimental_reuse_query_plans, - configuration + &configuration .persisted_queries .experimental_prewarm_query_plan_cache, ) @@ -294,34 +294,20 @@ impl YamlRouterFactory { ) -> Result { let query_planner_span = tracing::info_span!("query_planner_creation"); // QueryPlannerService takes an UnplannedRequest and outputs PlannedRequest - let bridge_query_planner = - match previous_supergraph.as_ref().map(|router| router.planners()) { - None => { - BridgeQueryPlannerPool::new( - schema.clone(), - configuration.clone(), - configuration - .supergraph - .query_planning - .experimental_query_planner_parallelism()?, - ) - .instrument(query_planner_span) - .await? - } - Some(planners) => { - BridgeQueryPlannerPool::new_from_planners( - planners, - schema.clone(), - configuration.clone(), - configuration - .supergraph - .query_planning - .experimental_query_planner_parallelism()?, - ) - .instrument(query_planner_span) - .await? - } - }; + let bridge_query_planner = BridgeQueryPlannerPool::new( + previous_supergraph + .as_ref() + .map(|router| router.js_planners()) + .unwrap_or_default(), + schema.clone(), + configuration.clone(), + configuration + .supergraph + .query_planning + .experimental_query_planner_parallelism()?, + ) + .instrument(query_planner_span) + .await?; let schema_changed = previous_supergraph .map(|supergraph_creator| supergraph_creator.schema().raw_sdl == schema.raw_sdl) @@ -511,8 +497,7 @@ pub async fn create_test_service_factory_from_yaml(schema: &str, configuration: .await; assert_eq!( service.map(|_| ()).unwrap_err().to_string().as_str(), - r#"couldn't build Query Planner Service: couldn't instantiate query planner; invalid schema: schema validation errors: Error extracting subgraphs from the supergraph: this might be due to errors in subgraphs that were mistakenly ignored by federation 0.x versions but are rejected by federation 2. -Please try composing your subgraphs with federation 2: this should help precisely pinpoint the problems and, once fixed, generate a correct federation 2 supergraph. + r#"couldn't build Query Planner Service: couldn't instantiate query planner; invalid schema: schema validation errors: Unexpected error extracting subgraphs from the supergraph: this is either a bug, or the supergraph has been corrupted. Details: Error: Cannot find type "Review" in subgraph "products" @@ -696,11 +681,11 @@ pub(crate) async fn create_plugins( add_optional_apollo_plugin!("preview_file_uploads"); add_optional_apollo_plugin!("preview_entity_cache"); add_mandatory_apollo_plugin!("progressive_override"); + add_optional_apollo_plugin!("demand_control"); // This relative ordering is documented in `docs/source/customizations/native.mdx`: add_optional_apollo_plugin!("rhai"); add_optional_apollo_plugin!("coprocessor"); - add_optional_apollo_plugin!("demand_control"); add_user_plugins!(); // Macros above remove from `apollo_plugin_factories`, so anything left at the end diff --git a/apollo-router/src/services/http/tests.rs b/apollo-router/src/services/http/tests.rs index de14b8d64f..892dc30e1b 100644 --- a/apollo-router/src/services/http/tests.rs +++ b/apollo-router/src/services/http/tests.rs @@ -452,51 +452,7 @@ async fn test_compressed_request_response_body() { ); } -const SCHEMA: &str = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query - subscription: Subscription - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - scalar join__FieldSet - enum join__Graph { - USER @join__graph(name: "user", url: "http://localhost:4001/graphql") - ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") - } - type Query { - currentUser: User @join__field(graph: USER) - } - - type Subscription @join__type(graph: USER) { - userWasCreated: User - } - - type User - @join__owner(graph: USER) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id"){ - id: ID! - name: String - activeOrganization: Organization - } - type Organization - @join__owner(graph: ORGA) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id") { - id: ID - creatorUser: User - name: String - nonNullId: ID! - suborga: [Organization] - }"#; +const SCHEMA: &str = include_str!("../../testdata/orga_supergraph.graphql"); struct TestPlugin { started: Arc, @@ -553,29 +509,48 @@ async fn test_http_plugin_is_loaded() { fn make_schema(path: &str) -> String { r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/authenticated/v0.1", for: SECURITY) { query: Query } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE + directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + + scalar link__Import + + enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION + } + scalar join__FieldSet enum join__Graph { USER @join__graph(name: "user", url: "unix://"#.to_string()+path+r#"") ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") } - type Query { + type Query + @join__type(graph: ORGA) + @join__type(graph: USER) + { currentUser: User @join__field(graph: USER) } type User - @join__owner(graph: USER) @join__type(graph: ORGA, key: "id") @join__type(graph: USER, key: "id"){ id: ID! diff --git a/apollo-router/src/services/layers/apq.rs b/apollo-router/src/services/layers/apq.rs index 6912c5e28c..d4f1119161 100644 --- a/apollo-router/src/services/layers/apq.rs +++ b/apollo-router/src/services/layers/apq.rs @@ -54,6 +54,14 @@ pub(crate) struct APQLayer { cache: Option>, } +impl APQLayer { + pub(crate) fn activate(&self) { + if let Some(cache) = &self.cache { + cache.activate(); + } + } +} + impl APQLayer { pub(crate) fn with_cache(cache: DeduplicatingCache) -> Self { Self { cache: Some(cache) } diff --git a/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs b/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs index c957b086e0..7051ec97be 100644 --- a/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs +++ b/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs @@ -686,7 +686,6 @@ mod tests { use super::*; use crate::configuration::Apq; use crate::configuration::PersistedQueries; - use crate::configuration::PersistedQueriesSafelist; use crate::test_harness::mocks::persisted_queries::*; use crate::uplink::Endpoints; @@ -783,15 +782,14 @@ mod tests { let manifest_manager = PersistedQueryManifestPoller::new( Configuration::fake_builder() .apq(Apq::fake_new(Some(false))) - .persisted_query(PersistedQueries::new( - Some(true), - Some(false), - Some(PersistedQueriesSafelist::default()), - Some(false), - Some(vec![ - "tests/fixtures/persisted-queries-manifest.json".to_string() - ]), - )) + .persisted_query( + PersistedQueries::builder() + .enabled(true) + .experimental_local_manifests(vec![ + "tests/fixtures/persisted-queries-manifest.json".to_string(), + ]) + .build(), + ) .build() .unwrap(), ) diff --git a/apollo-router/src/services/router/service.rs b/apollo-router/src/services/router/service.rs index f5013e5380..9e379ae3c4 100644 --- a/apollo-router/src/services/router/service.rs +++ b/apollo-router/src/services/router/service.rs @@ -873,6 +873,14 @@ impl RouterCreator { } else { APQLayer::disabled() }; + // There is a problem here. + // APQ isn't a plugin and so cannot participate in plugin lifecycle events. + // After telemetry `activate` NO part of the pipeline can fail as globals have been interacted with. + // However, the APQLayer uses DeduplicatingCache which is fallible. So if this fails on hot reload the router will be + // left in an inconsistent state and all metrics will likely stop working. + // Fixing this will require a larger refactor to bring APQ into the router lifecycle. + // For now just call activate to make the gauges work on the happy path. + apq_layer.activate(); Ok(Self { supergraph_creator, diff --git a/apollo-router/src/services/subgraph_service.rs b/apollo-router/src/services/subgraph_service.rs index 5d8fae1ede..2b99f3bd24 100644 --- a/apollo-router/src/services/subgraph_service.rs +++ b/apollo-router/src/services/subgraph_service.rs @@ -1744,6 +1744,7 @@ mod tests { } // starts a local server emulating a subgraph returning connection closed + #[cfg(not(target_os = "macos"))] async fn emulate_subgraph_panic(listener: TcpListener) { async fn handle(_request: http::Request) -> Result, Infallible> { panic!("test") @@ -2471,6 +2472,7 @@ mod tests { } #[tokio::test(flavor = "multi_thread")] + #[cfg(not(target_os = "macos"))] async fn test_subgraph_service_panic() { let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); let socket_addr = listener.local_addr().unwrap(); diff --git a/apollo-router/src/services/supergraph/service.rs b/apollo-router/src/services/supergraph/service.rs index dec84074f8..a77282d2db 100644 --- a/apollo-router/src/services/supergraph/service.rs +++ b/apollo-router/src/services/supergraph/service.rs @@ -28,6 +28,7 @@ use tracing_futures::Instrument; use crate::batching::BatchQuery; use crate::configuration::Batching; +use crate::configuration::PersistedQueriesPrewarmQueryPlanCache; use crate::context::OPERATION_NAME; use crate::error::CacheResolverError; use crate::graphql; @@ -797,6 +798,7 @@ impl PluggableSupergraphServiceBuilder { let schema = self.planner.schema(); let subgraph_schemas = self.planner.subgraph_schemas(); + let query_planner_service = CachingQueryPlanner::new( self.planner, schema.clone(), @@ -814,13 +816,9 @@ impl PluggableSupergraphServiceBuilder { } } - /*for (_, service) in self.subgraph_services.iter_mut() { - if let Some(subgraph) = - (service as &mut dyn std::any::Any).downcast_mut::() - { - subgraph.client_factory.plugins = plugins.clone(); - } - }*/ + // We need a non-fallible hook so that once we know we are going live with a pipeline we do final initialization. + // For now just shoe-horn something in, but if we ever reintroduce the query planner hook in plugins and activate then this can be made clean. + query_planner_service.activate(); let subgraph_service_factory = Arc::new(SubgraphServiceFactory::new( self.subgraph_services @@ -934,8 +932,8 @@ impl SupergraphCreator { self.query_planner_service.previous_cache() } - pub(crate) fn planners(&self) -> Vec>> { - self.query_planner_service.planners() + pub(crate) fn js_planners(&self) -> Vec>> { + self.query_planner_service.js_planners() } pub(crate) async fn warm_up_query_planner( @@ -945,7 +943,7 @@ impl SupergraphCreator { previous_cache: Option, count: Option, experimental_reuse_query_plans: bool, - experimental_pql_prewarm: bool, + experimental_pql_prewarm: &PersistedQueriesPrewarmQueryPlanCache, ) { self.query_planner_service .warm_up( diff --git a/apollo-router/src/services/supergraph/tests.rs b/apollo-router/src/services/supergraph/tests.rs index e624dd07a9..7a3d6765a1 100644 --- a/apollo-router/src/services/supergraph/tests.rs +++ b/apollo-router/src/services/supergraph/tests.rs @@ -18,51 +18,7 @@ use crate::Context; use crate::Notify; use crate::TestHarness; -const SCHEMA: &str = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query - subscription: Subscription - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - scalar join__FieldSet - enum join__Graph { - USER @join__graph(name: "user", url: "http://localhost:4001/graphql") - ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") - } - type Query { - currentUser: User @join__field(graph: USER) - } - - type Subscription @join__type(graph: USER) { - userWasCreated: User - } - - type User - @join__owner(graph: USER) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id"){ - id: ID! - name: String - activeOrganization: Organization - } - type Organization - @join__owner(graph: ORGA) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id") { - id: ID - creatorUser: User - name: String - nonNullId: ID! - suborga: [Organization] - }"#; +const SCHEMA: &str = include_str!("../../testdata/orga_supergraph.graphql"); #[tokio::test] async fn nullability_formatting() { @@ -289,52 +245,6 @@ fragment TestFragment on Query { #[tokio::test] async fn root_selection_skipped_with_other_fields() { - const SCHEMA: &str = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query - subscription: Subscription - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - scalar join__FieldSet - enum join__Graph { - USER @join__graph(name: "user", url: "http://localhost:4001/graphql") - ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") - } - type Query { - currentUser: User @join__field(graph: USER) - otherUser: User @join__field(graph: USER) - } - - type Subscription @join__type(graph: USER) { - userWasCreated: User - } - - type User - @join__owner(graph: USER) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id"){ - id: ID! - name: String - activeOrganization: Organization - } - type Organization - @join__owner(graph: ORGA) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id") { - id: ID - creatorUser: User - name: String - nonNullId: ID! - suborga: [Organization] - }"#; let subgraphs = MockedSubgraphs( [ ( @@ -2576,43 +2486,40 @@ async fn no_typename_on_interface() { .unwrap() .schema( r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query } - directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA - directive @join__field(graph: join__Graph, provides: join__FieldSet, requires: join__FieldSet) on FIELD_DEFINITION + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on INTERFACE | OBJECT + directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR - interface Animal { + interface Animal @join__type(graph: ANIMAL) { id: String! } - type Dog implements Animal { + type Dog implements Animal + @join__implements(graph: ANIMAL, interface: "Animal") + @join__type(graph: ANIMAL) + { id: String! name: String! } - type Query { + type Query @join__type(graph: ANIMAL) { animal: Animal! @join__field(graph: ANIMAL) dog: Dog! @join__field(graph: ANIMAL) } - enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION + scalar link__Import - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ + enum link__Purpose { SECURITY + EXECUTION } - scalar join__FieldSet enum join__Graph { @@ -2749,64 +2656,63 @@ async fn aliased_typename_on_fragments() { .unwrap() .schema( r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query } - directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA - directive @join__field(graph: join__Graph, provides: join__FieldSet, requires: join__FieldSet) on FIELD_DEFINITION + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT + directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR - directive @join__unionMember( - graph: join__Graph! - member: String! - ) repeatable on UNION + directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + + scalar link__Import - interface Animal { + enum link__Purpose { + SECURITY + EXECUTION + } + scalar join__FieldSet + + interface Animal + @join__type(graph: ANIMAL) + { id: String! } - type Dog implements Animal { + type Dog implements Animal + @join__implements(graph: ANIMAL, interface: "Animal") + @join__type(graph: ANIMAL) + { id: String! name: String! nickname: String! barkVolume: Int } - type Cat implements Animal { + type Cat implements Animal + @join__implements(graph: ANIMAL, interface: "Animal") + @join__type(graph: ANIMAL) + { id: String! name: String! nickname: String! meowVolume: Int } - type Query { + type Query @join__type(graph: ANIMAL){ animal: Animal! @join__field(graph: ANIMAL) dog: Dog! @join__field(graph: ANIMAL) } - enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION - - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY - } - union CatOrDog @join__type(graph: ANIMAL) @join__unionMember(graph: ANIMAL, member: "Dog") @join__unionMember(graph: ANIMAL, member: "Cat") = Cat | Dog - scalar join__FieldSet - enum join__Graph { ANIMAL @join__graph(name: "animal" url: "http://localhost:8080/query") } @@ -3097,18 +3003,10 @@ async fn interface_object_typename() { query: Query } - directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE - directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR - - directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION - directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA directive @owner( @@ -3126,14 +3024,7 @@ async fn interface_object_typename() { scalar link__Import enum link__Purpose { - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ SECURITY - - """ - `EXECUTION` features provide metadata necessary for operation execution. - """ EXECUTION } @@ -3292,40 +3183,50 @@ async fn interface_object_typename() { #[tokio::test] async fn fragment_reuse() { const SCHEMA: &str = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - scalar join__FieldSet - enum join__Graph { - USER @join__graph(name: "user", url: "http://localhost:4001/graphql") - ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") - } - type Query { - me: User @join__field(graph: USER) - } + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + directive @join__graph(name: String!, url: String!) on ENUM_VALUE + directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + directive @join__implements( graph: join__Graph! interface: String!) repeatable on OBJECT | INTERFACE - type User - @join__owner(graph: USER) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id"){ - id: ID! - name: String - organizations: [Organization] @join__field(graph: ORGA) - } - type Organization - @join__owner(graph: ORGA) - @join__type(graph: ORGA, key: "id") { - id: ID - name: String + scalar link__Import + + enum link__Purpose { + SECURITY + EXECUTION + } + scalar join__FieldSet + + enum join__Graph { + USER @join__graph(name: "user", url: "http://localhost:4001/graphql") + ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") + } + + type Query + @join__type(graph: ORGA) + @join__type(graph: USER) + { + me: User @join__field(graph: USER) + } + + type User + @join__type(graph: ORGA, key: "id") + @join__type(graph: USER, key: "id") + { + id: ID! + name: String + organizations: [Organization] @join__field(graph: ORGA) + } + type Organization + @join__type(graph: ORGA, key: "id") + { + id: ID + name: String @join__field(graph: ORGA) }"#; let subgraphs = MockedSubgraphs([ @@ -3393,18 +3294,11 @@ async fn abstract_types_in_requires() { query: Query } - directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE - directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR - directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION - directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA scalar join__FieldSet @@ -3417,14 +3311,7 @@ async fn abstract_types_in_requires() { scalar link__Import enum link__Purpose { - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ SECURITY - - """ - `EXECUTION` features provide metadata necessary for operation execution. - """ EXECUTION } @@ -3555,28 +3442,34 @@ async fn abstract_types_in_requires() { } const ENUM_SCHEMA: &str = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + + scalar link__Import + + enum link__Purpose { + SECURITY + EXECUTION + } + scalar join__FieldSet + enum join__Graph { USER @join__graph(name: "user", url: "http://localhost:4001/graphql") ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") } - type Query { + type Query @join__type(graph: USER) @join__type(graph: ORGA){ test(input: InputEnum): String @join__field(graph: USER) } - enum InputEnum { + enum InputEnum @join__type(graph: USER) @join__type(graph: ORGA) { A B }"#; diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-10.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-10.snap new file mode 100644 index 0000000000..b6f6e5c2d4 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-10.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query Test($a: String, $b: String) { + ...TestFragment @hasArg(arg: $a) + ...TestFragment2 @hasArg(arg: $b) + c + } + + fragment TestFragment on Query { + __typename @remove + } + + fragment TestFragment2 on Query { + __typename + } + +filtered: +query Test($b: String) { + ...TestFragment2 @hasArg(arg: $b) + c +} + +fragment TestFragment2 on Query { + __typename +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-11.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-11.snap new file mode 100644 index 0000000000..3f17858f2e --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-11.snap @@ -0,0 +1,23 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query Test($a: String, $b: String) { + ... @hasArg(arg: $a) { + c @remove + } + ... @hasArg(arg: $b) { + test: c + } + c + } + +filtered: +query Test($b: String) { + ... @hasArg(arg: $b) { + test: c + } + c +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-12.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-12.snap new file mode 100644 index 0000000000..58212cd6b3 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-12.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String, $b: String) { + c + f(arg: [["a"], [$a], ["b"]]) @remove + aliased: f(arg: [["a"], [$b]]) + } + +filtered: +query($b: String) { + c + aliased: f(arg: [["a"], [$b]]) +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-13.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-13.snap new file mode 100644 index 0000000000..17670d39e6 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-13.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String, $b: String) { + c + g(arg: [{a: $a}, {a: "a"}]) @remove + aliased: g(arg: [{a: "a"}, {a: $b}]) + } + +filtered: +query($b: String) { + c + aliased: g(arg: [{a: "a"}, {a: $b}]) +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-14.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-14.snap new file mode 100644 index 0000000000..938fea77c7 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-14.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String, $b: String) { + c + e(arg: {c: [$a]}) @remove + aliased: e(arg: {c: [$b]}) + } + +filtered: +query($b: String) { + c + aliased: e(arg: {c: [$b]}) +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-2.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-2.snap new file mode 100644 index 0000000000..4f85ed0409 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-2.snap @@ -0,0 +1,14 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String) { + a(arg: $a) @remove + c + } +filtered: +{ + c +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-3.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-3.snap new file mode 100644 index 0000000000..de9dd21421 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-3.snap @@ -0,0 +1,18 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String) { + ... F + c + } + + fragment F on Query { + a(arg: $a) @remove + } +filtered: +{ + c +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-4.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-4.snap new file mode 100644 index 0000000000..160529a1de --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-4.snap @@ -0,0 +1,18 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String) { + ... F @remove + c + } + + fragment F on Query { + a(arg: $a) + } +filtered: +{ + c +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-5.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-5.snap new file mode 100644 index 0000000000..5f40386191 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-5.snap @@ -0,0 +1,23 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String) { + ... F @remove + c + } + + fragment F on Query { + ... G + } + + fragment G on Query { + a(arg: $a) + } + +filtered: +{ + c +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-6.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-6.snap new file mode 100644 index 0000000000..d2f11f22a9 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-6.snap @@ -0,0 +1,23 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String) { + ... F + c + } + + fragment F on Query { + ... G + } + + fragment G on Query { + a(arg: $a) @remove + } + +filtered: +{ + c +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-7.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-7.snap new file mode 100644 index 0000000000..f0576d4b3d --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-7.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String, $b: String) { + c + d(arg: ["a", $a, "b"]) @remove + aliased: d(arg: [$b]) + } + +filtered: +query($b: String) { + c + aliased: d(arg: [$b]) +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-8.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-8.snap new file mode 100644 index 0000000000..dbe3c6cf62 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-8.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query($a: String, $b: String) { + c + e(arg: {a: $a, b: "b"}) @remove + aliased: e(arg: {a: "a", b: $b}) + } + +filtered: +query($b: String) { + c + aliased: e(arg: {a: "a", b: $b}) +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-9.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-9.snap new file mode 100644 index 0000000000..9b149e9db6 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive-9.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query Test($a: String, $b: String, $c: String) @hasArg(arg: $a) { + ...TestFragment + ...TestFragment2 + c + } + + fragment TestFragment on Query @hasArg(arg: $b) { + __typename @remove + } + + fragment TestFragment2 on Query @hasArg(arg: $c) { + __typename + } + +filtered: +query Test($a: String, $c: String) @hasArg(arg: $a) { + ...TestFragment2 + c +} + +fragment TestFragment2 on Query @hasArg(arg: $c) { + __typename +} diff --git a/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive.snap b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive.snap new file mode 100644 index 0000000000..3740848613 --- /dev/null +++ b/apollo-router/src/spec/query/snapshots/apollo_router__spec__query__transform__tests__remove_directive.snap @@ -0,0 +1,20 @@ +--- +source: apollo-router/src/spec/query/transform.rs +expression: "TestResult { query, result }" +--- +query: + + query { + a + ... F @remove + } + + fragment F on Query { + b { + a + } + } +filtered: +{ + a +} diff --git a/apollo-router/src/spec/query/tests.rs b/apollo-router/src/spec/query/tests.rs index 11f078af5d..bb13004402 100644 --- a/apollo-router/src/spec/query/tests.rs +++ b/apollo-router/src/spec/query/tests.rs @@ -118,8 +118,8 @@ impl FormatTest { let query_type_name = self.query_type_name.unwrap_or("Query"); let schema = match self.federation_version { - FederationVersion::Fed1 => with_supergraph_boilerplate(schema, query_type_name), - FederationVersion::Fed2 => with_supergraph_boilerplate_fed2(schema, query_type_name), + FederationVersion::Fed1 => with_supergraph_boilerplate_fed1(schema, query_type_name), + FederationVersion::Fed2 => with_supergraph_boilerplate(schema, query_type_name), }; let schema = Schema::parse(&schema, &Default::default()).expect("could not parse schema"); @@ -161,7 +161,7 @@ impl FormatTest { } } -fn with_supergraph_boilerplate(content: &str, query_type_name: &str) -> String { +fn with_supergraph_boilerplate_fed1(content: &str, query_type_name: &str) -> String { format!( r#" schema @@ -173,7 +173,7 @@ fn with_supergraph_boilerplate(content: &str, query_type_name: &str) -> String { }} directive @core(feature: String!) repeatable on SCHEMA directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + directive @inaccessible on OBJECT| FIELD_DEFINITION | INTERFACE | UNION enum join__Graph {{ TEST @join__graph(name: "test", url: "http://localhost:4001/graphql") }} @@ -183,36 +183,28 @@ fn with_supergraph_boilerplate(content: &str, query_type_name: &str) -> String { ) } -fn with_supergraph_boilerplate_fed2(content: &str, query_type_name: &str) -> String { +fn with_supergraph_boilerplate(content: &str, query_type_name: &str) -> String { format!( r#" schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.2", for: EXECUTION) @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.2", for: EXECUTION) {{ query: {query_type_name} }} directive @join__field(graph: join__Graph!, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA - directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ENUM | ENUM_VALUE | SCALAR | INPUT_OBJECT | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION + directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION scalar join__FieldSet scalar link__Import enum link__Purpose {{ - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY - - """ - `EXECUTION` features provide metadata necessary for operation execution. - """ - EXECUTION + SECURITY + EXECUTION }} enum join__Graph {{ @@ -1788,16 +1780,24 @@ fn variable_validation() { let schema = r#" schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + { query: Query mutation: Mutation } - directive @core(feature: String!) repeatable on SCHEMA + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + directive @join__type( graph: join__Graph! key: join__FieldSet extension: Boolean! = false resolvable: Boolean! = true isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + + scalar join__FieldSet + scalar link__Import + + enum link__Purpose { + SECURITY + EXECUTION + } + enum join__Graph { TEST @join__graph(name: "test", url: "http://localhost:4001/graphql") } @@ -1805,7 +1805,7 @@ fn variable_validation() { type Mutation{ foo(input: FooInput!): FooResponse! } - type Query{ + type Query @join__type(graph: TEST){ data: String } @@ -5074,37 +5074,26 @@ fn fragment_on_interface_on_query() { let schema = r#"schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.2", for: EXECUTION) - @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) { query: MyQueryObject } - directive @join__field(graph: join__Graph!, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA - directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ENUM | ENUM_VALUE | SCALAR | INPUT_OBJECT | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION scalar join__FieldSet scalar link__Import enum link__Purpose { - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY - - """ - `EXECUTION` features provide metadata necessary for operation execution. - """ - EXECUTION + SECURITY + EXECUTION } enum join__Graph { TEST @join__graph(name: "test", url: "http://localhost:4001/graphql") } - type MyQueryObject implements Interface { + type MyQueryObject implements Interface @join__type(graph: TEST){ object: MyObject other: String } @@ -5690,20 +5679,35 @@ fn test_query_not_named_query() { let schema = Schema::parse( r#" schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + { query: TheOneAndOnlyQuery } - directive @core(feature: String!) repeatable on SCHEMA + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + + scalar join__FieldSet + scalar link__Import + + enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION + } + enum join__Graph { TEST @join__graph(name: "test", url: "http://localhost:4001/graphql") } - type TheOneAndOnlyQuery { example: Boolean } + type TheOneAndOnlyQuery @join__type(graph: TEST) { example: Boolean } "#, &Default::default(), ) @@ -5729,20 +5733,27 @@ fn filtered_defer_fragment() { let schema = Schema::parse( r#" schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + { + query: Query } - directive @core(feature: String!) repeatable on SCHEMA + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + + scalar join__FieldSet + scalar link__Import + + enum link__Purpose { + SECURITY + EXECUTION + } enum join__Graph { TEST @join__graph(name: "test", url: "http://localhost:4001/graphql") } - type Query { + type Query @join__type(graph: TEST) { a: A } diff --git a/apollo-router/src/spec/query/transform.rs b/apollo-router/src/spec/query/transform.rs index b307d9a2d8..8b31d6054b 100644 --- a/apollo-router/src/spec/query/transform.rs +++ b/apollo-router/src/spec/query/transform.rs @@ -1,8 +1,9 @@ +use std::collections::BTreeMap; use std::collections::HashMap; +use std::collections::HashSet; use apollo_compiler::ast; use apollo_compiler::schema::FieldLookupError; -use apollo_compiler::Name; use tower::BoxError; /// Transform a document with the given visitor. @@ -15,17 +16,46 @@ pub(crate) fn document( definitions: Vec::new(), }; - // walk through the fragment first: if a fragment is entirely filtered, we want to + // go through the fragments and order them, starting with the ones that reference no other fragments + // then the ones that depend only on the first one, and so on + // This allows visitors like authorization to have all the required information if they encounter + // a fragment spread while filtering a fragment + let mut fragment_visitor = FragmentOrderVisitor::new(); + fragment_visitor.visit_document(document); + let ordered_fragments = fragment_visitor.ordered_fragments(); + + visitor.state().reset(); + + // Then walk again through the fragments: if a fragment is entirely filtered, we want to // remove the spread too - for definition in &document.definitions { - if let ast::Definition::FragmentDefinition(def) = definition { - if let Some(new_def) = visitor.fragment_definition(def)? { - new.definitions - .push(ast::Definition::FragmentDefinition(new_def.into())) - } + for def in ordered_fragments { + visitor.state().used_fragments.clear(); + visitor.state().used_variables.clear(); + + if let Some(new_def) = visitor.fragment_definition(def)? { + // keep the list of used variables per fragment, as we need to use it to know which variables are used + // in a query + let used_variables = visitor.state().used_variables.clone(); + + // keep the list of used fragments per fragment, as we need to use it to gather used variables later + // unfortunately, we may not know the variable used for those fragments at this point, as they may not + // have been processed yet + let local_used_fragments = visitor.state().used_fragments.clone(); + + visitor.state().defined_fragments.insert( + def.name.as_str().to_string(), + DefinedFragment { + fragment: new_def, + used_variables, + used_fragments: local_used_fragments, + }, + ); } } + // keeps the list of fragments used in the produced document (some fragment spreads might have been removed) + let mut used_fragments = HashSet::new(); + for definition in &document.definitions { if let ast::Definition::OperationDefinition(def) = definition { let root_type = visitor @@ -33,18 +63,116 @@ pub(crate) fn document( .root_operation(def.operation_type) .ok_or("missing root operation definition")? .clone(); - if let Some(new_def) = visitor.operation(&root_type, def)? { + + // we reset the used_fragments and used_variables lists for each operation + visitor.state().used_fragments.clear(); + visitor.state().used_variables.clear(); + if let Some(mut new_def) = visitor.operation(&root_type, def)? { + let mut local_used_fragments = visitor.state().used_fragments.clone(); + + // gather the entire list of fragments used in this operation + loop { + let mut new_local_used_fragments = local_used_fragments.clone(); + for fragment_name in local_used_fragments.iter() { + if let Some(defined_fragment) = visitor + .state() + .defined_fragments + .get(fragment_name.as_str()) + { + new_local_used_fragments + .extend(defined_fragment.used_fragments.clone()); + } + } + + // no more changes, we can stop + if new_local_used_fragments.len() == local_used_fragments.len() { + break; + } + local_used_fragments = new_local_used_fragments; + } + + // add to the list of used variables all the variables used in the fragment spreads + for fragment_name in local_used_fragments.iter() { + if let Some(defined_fragment_used_variables) = visitor + .state() + .defined_fragments + .get(fragment_name.as_str()) + .map(|defined_fragment| defined_fragment.used_variables.clone()) + { + visitor + .state() + .used_variables + .extend(defined_fragment_used_variables); + } + } + used_fragments.extend(local_used_fragments); + + // remove unused variables + new_def.variables.retain(|var| { + let res = visitor.state().used_variables.contains(var.name.as_str()); + res + }); + new.definitions - .push(ast::Definition::OperationDefinition(new_def.into())) + .push(ast::Definition::OperationDefinition(new_def.into())); } } } + + for (name, defined_fragment) in visitor.state().defined_fragments.clone().into_iter() { + if used_fragments.contains(name.as_str()) { + new.definitions.push(ast::Definition::FragmentDefinition( + defined_fragment.fragment.into(), + )); + } + } Ok(new) } +/// Holds state during the transformation to account for used fragments and variables. +pub(crate) struct TransformState { + used_fragments: HashSet, + used_variables: HashSet, + /// keeps the list of fragments defined in the produced document (the visitor might have removed some of them) + defined_fragments: BTreeMap, +} + +#[derive(Clone)] +pub(crate) struct DefinedFragment { + pub(crate) fragment: ast::FragmentDefinition, + /// variables used in the fragment + pub(crate) used_variables: HashSet, + /// fragments used in the fragment + pub(crate) used_fragments: HashSet, +} + +impl TransformState { + pub(crate) fn new() -> Self { + Self { + used_fragments: HashSet::new(), + used_variables: HashSet::new(), + defined_fragments: BTreeMap::new(), + } + } + + fn reset(&mut self) { + self.used_fragments.clear(); + self.used_variables.clear(); + self.defined_fragments.clear(); + } + + pub(crate) fn fragments(&self) -> &BTreeMap { + &self.defined_fragments + } +} + pub(crate) trait Visitor: Sized { fn schema(&self) -> &apollo_compiler::Schema; + /// mutable state provided by the visitor to clean up unused fragments and variables + /// do not modify directly + fn state(&mut self) -> &mut TransformState; + /// Transform an operation definition. /// /// Call the [`operation`] free function for the default behavior. @@ -89,7 +217,13 @@ pub(crate) trait Visitor: Sized { &mut self, def: &ast::FragmentSpread, ) -> Result, BoxError> { - fragment_spread(self, def) + let res = fragment_spread(self, def); + if let Ok(Some(ref fragment)) = res.as_ref() { + self.state() + .used_fragments + .insert(fragment.fragment_name.as_str().to_string()); + } + res } /// Transform a inline fragment within a selection set. @@ -117,6 +251,12 @@ pub(crate) fn operation( return Ok(None); }; + for directive in def.directives.iter() { + for argument in directive.arguments.iter() { + used_variables_from_value(visitor, &argument.value); + } + } + Ok(Some(ast::OperationDefinition { name: def.name.clone(), operation_type: def.operation_type, @@ -137,6 +277,13 @@ pub(crate) fn fragment_definition( else { return Ok(None); }; + + for directive in def.directives.iter() { + for argument in directive.arguments.iter() { + used_variables_from_value(visitor, &argument.value); + } + } + Ok(Some(ast::FragmentDefinition { name: def.name.clone(), type_condition: def.type_condition.clone(), @@ -158,6 +305,17 @@ pub(crate) fn field( else { return Ok(None); }; + + for argument in def.arguments.iter() { + used_variables_from_value(visitor, &argument.value); + } + + for directive in def.directives.iter() { + for argument in directive.arguments.iter() { + used_variables_from_value(visitor, &argument.value); + } + } + Ok(Some(ast::Field { alias: def.alias.clone(), name: def.name.clone(), @@ -174,7 +332,17 @@ pub(crate) fn fragment_spread( visitor: &mut impl Visitor, def: &ast::FragmentSpread, ) -> Result, BoxError> { - let _ = visitor; // Unused, but matches trait method signature + visitor + .state() + .used_fragments + .insert(def.fragment_name.as_str().to_string()); + + for directive in def.directives.iter() { + for argument in directive.arguments.iter() { + used_variables_from_value(visitor, &argument.value); + } + } + Ok(Some(def.clone())) } @@ -189,6 +357,13 @@ pub(crate) fn inline_fragment( let Some(selection_set) = selection_set(visitor, parent_type, &def.selection_set)? else { return Ok(None); }; + + for directive in def.directives.iter() { + for argument in directive.arguments.iter() { + used_variables_from_value(visitor, &argument.value); + } + } + Ok(Some(ast::InlineFragment { type_condition: def.type_condition.clone(), directives: def.directives.clone(), @@ -242,50 +417,179 @@ pub(crate) fn selection_set( Ok((!selections.is_empty()).then_some(selections)) } -pub(crate) fn collect_fragments( - executable: &ast::Document, -) -> HashMap<&Name, &ast::FragmentDefinition> { - executable - .definitions - .iter() - .filter_map(|def| match def { - ast::Definition::FragmentDefinition(frag) => Some((&frag.name, frag.as_ref())), - _ => None, - }) - .collect() +fn used_variables_from_value( + visitor: &mut V, + argument_value: &apollo_compiler::ast::Value, +) { + match argument_value { + apollo_compiler::ast::Value::Variable(name) => { + visitor + .state() + .used_variables + .insert(name.as_str().to_string()); + } + apollo_compiler::ast::Value::List(values) => { + for value in values { + used_variables_from_value(visitor, value); + } + } + apollo_compiler::ast::Value::Object(values) => { + for (_, value) in values { + used_variables_from_value(visitor, value); + } + } + _ => {} + } +} + +/// this visitor goes through the list of fragments in the query, looking at fragment spreads +/// in their selection, and generates a list of fragments in the order they should be visited +/// by the transform visitor, to ensure a fragment has already been visited before it is +/// referenced in a fragment spread +struct FragmentOrderVisitor<'a> { + // the resulting list of ordered fragments + ordered_fragments: Vec, + // list of fragments in the document + fragments: HashMap, + + // fragment dependencies. The key is a fragment name, the value is all the fragments that reference it + // in a fragment spread + dependencies: HashMap>, + // name of the fragment currently being visited + current: Option, + + // how many fragments are used by each fragment. This is decremented when a referenced fragment + // is added to the final list. Once it reaches 0, the fragment is added to the final list too + rank: HashMap, } -#[test] -fn test_add_directive_to_fields() { - struct AddDirective { - schema: apollo_compiler::Schema, +impl<'a> FragmentOrderVisitor<'a> { + fn new() -> Self { + Self { + ordered_fragments: Vec::new(), + fragments: HashMap::new(), + dependencies: HashMap::new(), + current: None, + rank: HashMap::new(), + } } - impl Visitor for AddDirective { - fn field( - &mut self, - _parent_type: &str, - field_def: &ast::FieldDefinition, - def: &ast::Field, - ) -> Result, BoxError> { - Ok(field(self, field_def, def)?.map(|mut new| { - new.directives.push( - ast::Directive { - name: apollo_compiler::name!("added"), - arguments: Vec::new(), + fn rerank(&mut self, name: &str) { + if let Some(v) = self.dependencies.remove(name) { + for dep in v { + if let Some(rank) = self.rank.get_mut(&dep) { + *rank -= 1; + if *rank == 0 { + self.ordered_fragments.push(dep.clone()); + self.rerank(&dep); } - .into(), - ); - new - })) + } + } } + } - fn schema(&self) -> &apollo_compiler::Schema { - &self.schema + fn ordered_fragments(self) -> Vec<&'a ast::FragmentDefinition> { + let mut ordered_fragments = Vec::new(); + for name in self.ordered_fragments { + if let Some(fragment) = self.fragments.get(name.as_str()) { + ordered_fragments.push(*fragment); + } + } + ordered_fragments + } + + fn visit_document(&mut self, doc: &'a ast::Document) { + for definition in &doc.definitions { + if let ast::Definition::FragmentDefinition(def) = definition { + self.visit_fragment_definition(def); + } + } + } + + fn visit_fragment_definition(&mut self, def: &'a ast::FragmentDefinition) { + let name = def.name.as_str().to_string(); + self.fragments.insert(name.clone(), def); + + self.current = Some(name.clone()); + self.rank.insert(name.clone(), 0); + + self.visit_selection_set(&def.selection_set); + + if self.rank.get(&name) == Some(&0) { + // if the fragment does not reference any other fragments, it is ready to be added to the final list + self.ordered_fragments.push(name.clone()); + // then we rerank all the fragments that reference this one: if any of them reaches the rank 0, they + // are added to the final list too + self.rerank(&name); + } + } + + fn visit_selection_set(&mut self, selection_set: &[apollo_compiler::ast::Selection]) { + for selection in selection_set { + match selection { + ast::Selection::Field(def) => self.visit_selection_set(&def.selection_set), + ast::Selection::InlineFragment(def) => self.visit_selection_set(&def.selection_set), + ast::Selection::FragmentSpread(def) => { + let name = def.fragment_name.as_str().to_string(); + + // we have already seen this fragment, so we don't need to add it again + if self.rank.get(name.as_str()) == Some(&0) { + continue; + } + if let Some(current) = self.current.as_ref() { + if let Some(rank) = self.rank.get_mut(current.as_str()) { + *rank += 1; + } + self.dependencies + .entry(name) + .or_default() + .push(current.clone()); + } + } + } } } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_add_directive_to_fields() { + struct AddDirective { + schema: apollo_compiler::Schema, + state: TransformState, + } + + impl Visitor for AddDirective { + fn field( + &mut self, + _parent_type: &str, + field_def: &ast::FieldDefinition, + def: &ast::Field, + ) -> Result, BoxError> { + Ok(field(self, field_def, def)?.map(|mut new| { + new.directives.push( + ast::Directive { + name: apollo_compiler::name!("added"), + arguments: Vec::new(), + } + .into(), + ); + new + })) + } + + fn schema(&self) -> &apollo_compiler::Schema { + &self.schema + } + + fn state(&mut self) -> &mut TransformState { + &mut self.state + } + } - let graphql = " + let graphql = " type Query { a(id: ID): String b: Int @@ -307,23 +611,367 @@ fn test_add_directive_to_fields() { } } "; - let ast = apollo_compiler::ast::Document::parse(graphql, "").unwrap(); - let (schema, _doc) = ast.to_mixed_validate().unwrap(); - let schema = schema.into_inner(); - let mut visitor = AddDirective { schema }; - let expected = "fragment F on Query { - next @added { - a @added - } -} - -query($id: ID = null) { + let ast = apollo_compiler::ast::Document::parse(graphql, "").unwrap(); + let (schema, _doc) = ast.to_mixed_validate().unwrap(); + let schema = schema.into_inner(); + let mut visitor = AddDirective { + schema, + state: TransformState::new(), + }; + let expected = "query($id: ID = null) { a(id: $id) @added ... @defer { b @added } ...F } + +fragment F on Query { + next @added { + a @added + } +} "; - assert_eq!(document(&mut visitor, &ast).unwrap().to_string(), expected) + assert_eq!(document(&mut visitor, &ast).unwrap().to_string(), expected) + } + + struct RemoveDirective { + schema: apollo_compiler::Schema, + state: TransformState, + } + + impl RemoveDirective { + fn new(schema: apollo_compiler::Schema) -> Self { + Self { + schema, + state: TransformState::new(), + } + } + } + + impl Visitor for RemoveDirective { + fn field( + &mut self, + _parent_type: &str, + field_def: &ast::FieldDefinition, + def: &ast::Field, + ) -> Result, BoxError> { + if def.directives.iter().any(|d| d.name == "remove") { + return Ok(None); + } + field(self, field_def, def) + } + + fn fragment_spread( + &mut self, + def: &ast::FragmentSpread, + ) -> Result, BoxError> { + if def.directives.iter().any(|d| d.name == "remove") { + return Ok(None); + } + + // remove the fragment spread if the fragment was removed + if !self + .state() + .fragments() + .contains_key(def.fragment_name.as_str()) + { + return Ok(None); + } + + fragment_spread(self, def) + } + + fn inline_fragment( + &mut self, + _parent_type: &str, + def: &ast::InlineFragment, + ) -> Result, BoxError> { + if def.directives.iter().any(|d| d.name == "remove") { + return Ok(None); + } + inline_fragment(self, _parent_type, def) + } + + fn schema(&self) -> &apollo_compiler::Schema { + &self.schema + } + + fn state(&mut self) -> &mut TransformState { + &mut self.state + } + } + + struct TestResult<'a> { + query: &'a str, + result: ast::Document, + } + + impl<'a> std::fmt::Display for TestResult<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "query:\n{}\nfiltered:\n{}", self.query, self.result,) + } + } + + static TRANSFORM_REMOVE_SCHEMA: &str = r#" + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/authenticated/v0.1", for: SECURITY) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @remove on FIELD | INLINE_FRAGMENT | FRAGMENT_SPREAD + directive @hasArg(arg: String!) on QUERY | FRAGMENT_DEFINITION | INLINE_FRAGMENT | FRAGMENT_SPREAD + scalar link__Import + enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION + } + + type Query { + a(arg: String): String + b: Obj + c: Int + d(arg: [String]): String + e(arg: Inp): String + f(arg: [[String]]): String + g(arg: [Inp]): String + + } + + input Inp { + a: String + b: String + c: [String] + } + + type Obj { + a: String + } + "#; + + #[test] + fn remove_directive() { + let ast = apollo_compiler::ast::Document::parse(TRANSFORM_REMOVE_SCHEMA, "").unwrap(); + let (schema, _doc) = ast.to_mixed_validate().unwrap(); + let schema = schema.into_inner(); + let mut visitor = RemoveDirective::new(schema.clone()); + + // test removed fragment + let query = r#" + query { + a + ... F @remove + } + + fragment F on Query { + b { + a + } + }"#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable + let query = r#" + query($a: String) { + a(arg: $a) @remove + c + }"#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in fragment + let query = r#" + query($a: String) { + ... F + c + } + + fragment F on Query { + a(arg: $a) @remove + }"#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test field with variable in removed fragment + let query = r#" + query($a: String) { + ... F @remove + c + } + + fragment F on Query { + a(arg: $a) + }"#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test field with variable in fragment nested in removed fragment + let query = r#" + query($a: String) { + ... F @remove + c + } + + fragment F on Query { + ... G + } + + fragment G on Query { + a(arg: $a) + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in fragment nested in fragment + let query = r#" + query($a: String) { + ... F + c + } + + fragment F on Query { + ... G + } + + fragment G on Query { + a(arg: $a) @remove + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in list argument + let query = r#" + query($a: String, $b: String) { + c + d(arg: ["a", $a, "b"]) @remove + aliased: d(arg: [$b]) + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in input argument + let query = r#" + query($a: String, $b: String) { + c + e(arg: {a: $a, b: "b"}) @remove + aliased: e(arg: {a: "a", b: $b}) + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in directive on operation and fragment + let query = r#" + query Test($a: String, $b: String, $c: String) @hasArg(arg: $a) { + ...TestFragment + ...TestFragment2 + c + } + + fragment TestFragment on Query @hasArg(arg: $b) { + __typename @remove + } + + fragment TestFragment2 on Query @hasArg(arg: $c) { + __typename + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in directive on fragment spread + let query = r#" + query Test($a: String, $b: String) { + ...TestFragment @hasArg(arg: $a) + ...TestFragment2 @hasArg(arg: $b) + c + } + + fragment TestFragment on Query { + __typename @remove + } + + fragment TestFragment2 on Query { + __typename + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in directive on inline fragment + let query = r#" + query Test($a: String, $b: String) { + ... @hasArg(arg: $a) { + c @remove + } + ... @hasArg(arg: $b) { + test: c + } + c + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in nested list argument + let query = r#" + query($a: String, $b: String) { + c + f(arg: [["a"], [$a], ["b"]]) @remove + aliased: f(arg: [["a"], [$b]]) + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in input type in list argument + let query = r#" + query($a: String, $b: String) { + c + g(arg: [{a: $a}, {a: "a"}]) @remove + aliased: g(arg: [{a: "a"}, {a: $b}]) + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + + // test removed field with variable in list in input type argument + let query = r#" + query($a: String, $b: String) { + c + e(arg: {c: [$a]}) @remove + aliased: e(arg: {c: [$b]}) + } + "#; + let doc = ast::Document::parse(query, "query.graphql").unwrap(); + let result = document(&mut visitor, &doc).unwrap(); + insta::assert_snapshot!(TestResult { query, result }); + } } diff --git a/apollo-router/src/spec/schema.rs b/apollo-router/src/spec/schema.rs index 05546910ca..edb6c3bbac 100644 --- a/apollo-router/src/spec/schema.rs +++ b/apollo-router/src/spec/schema.rs @@ -361,12 +361,23 @@ mod tests { "{}\n{}", r#" schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") { + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + { query: Query } - directive @core(feature: String!) repeatable on SCHEMA + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR directive @join__graph(name: String!, url: String!) on ENUM_VALUE + + scalar link__Import + scalar join__FieldSet + + enum link__Purpose { + SECURITY + EXECUTION + } + enum join__Graph { TEST @join__graph(name: "test", url: "http://localhost:4001/graphql") } @@ -457,27 +468,7 @@ mod tests { #[test] fn routing_urls() { - let schema = r#" - schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") - { - query: Query - } - type Query { - me: String - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - - enum join__Graph { - ACCOUNTS @join__graph(name:"accounts" url: "http://localhost:4001/graphql") - INVENTORY - @join__graph(name: "inventory", url: "http://localhost:4004/graphql") - PRODUCTS - @join__graph(name: "products" url: "http://localhost:4003/graphql") - REVIEWS @join__graph(name: "reviews" url: "http://localhost:4002/graphql") - }"#; + let schema = include_str!("../testdata/minimal_local_inventory_supergraph.graphql"); let schema = Schema::parse(schema, &Default::default()).unwrap(); assert_eq!(schema.subgraphs.len(), 4); @@ -497,7 +488,7 @@ mod tests { .get("inventory") .map(|s| s.to_string()) .as_deref(), - Some("http://localhost:4004/graphql"), + Some("http://localhost:4002/graphql"), "Incorrect url for inventory" ); @@ -517,7 +508,7 @@ mod tests { .get("reviews") .map(|s| s.to_string()) .as_deref(), - Some("http://localhost:4002/graphql"), + Some("http://localhost:4004/graphql"), "Incorrect url for reviews" ); @@ -543,7 +534,7 @@ mod tests { fn federation_version() { // @core directive let schema = Schema::parse( - include_str!("../testdata/minimal_supergraph.graphql"), + include_str!("../testdata/minimal_fed1_supergraph.graphql"), &Default::default(), ) .unwrap(); @@ -551,7 +542,7 @@ mod tests { // @link directive let schema = Schema::parse( - include_str!("../testdata/minimal_fed2_supergraph.graphql"), + include_str!("../testdata/minimal_supergraph.graphql"), &Default::default(), ) .unwrap(); @@ -567,7 +558,7 @@ mod tests { assert_eq!( Schema::schema_id(&schema.raw_sdl), - "8e2021d131b23684671c3b85f82dfca836908c6a541bbd5c3772c66e7f8429d8".to_string() + "23bcf0ea13a4e0429c942bba59573ba70b8d6970d73ad00c5230d08788bb1ba2".to_string() ); } } diff --git a/apollo-router/src/testdata/a_b_supergraph.graphql b/apollo-router/src/testdata/a_b_supergraph.graphql new file mode 100644 index 0000000000..d51619dfa0 --- /dev/null +++ b/apollo-router/src/testdata/a_b_supergraph.graphql @@ -0,0 +1,60 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query + mutation: Mutation +} + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar join__FieldSet +scalar link__Import + +enum join__Graph { + A @join__graph(name: "A", url: "http://localhost:4001") + B @join__graph(name: "B", url: "http://localhost:4004") +} + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation @join__type(graph: A) @join__type(graph: B) { + mutationA: Mutation @join__field(graph: A) + mutationB: Boolean @join__field(graph: B) +} + +type Query @join__type(graph: A) @join__type(graph: B) { + query: Boolean @join__field(graph: A) +} diff --git a/apollo-router/src/testdata/contract_schema.graphql b/apollo-router/src/testdata/contract_schema.graphql index 74db321db8..12ee987bf6 100644 --- a/apollo-router/src/testdata/contract_schema.graphql +++ b/apollo-router/src/testdata/contract_schema.graphql @@ -1,40 +1,119 @@ -schema @core(feature: "https://specs.apollo.dev/core/v0.1") @core(feature: "https://specs.apollo.dev/join/v0.1") @core(feature: "https://specs.apollo.dev/tag/v0.1") @apollo_studio_metadata(launchId: "2396d4fb-a1e4-457d-8da4-347479b852f1", buildId: "2396d4fb-a1e4-457d-8da4-347479b852f1", checkId: null) @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") { +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @apollo_studio_metadata( + launchId: "2396d4fb-a1e4-457d-8da4-347479b852f1" + buildId: "2396d4fb-a1e4-457d-8da4-347479b852f1" + checkId: null + ) { query: Query mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR -directive @apollo_studio_metadata(launchId: String, buildId: String, checkId: String) on SCHEMA +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + directive @tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT + | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE + | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA scalar join__FieldSet +directive @apollo_studio_metadata( + launchId: String + buildId: String + checkId: String +) on SCHEMA + enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "http://accounts.demo.starstuff.dev/graphql") - INVENTORY @join__graph(name: "inventory", url: "http://inventory.demo.starstuff.dev/graphql") - PRODUCTS @join__graph(name: "products", url: "http://products.demo.starstuff.dev/graphql") - REVIEWS @join__graph(name: "reviews", url: "http://reviews.demo.starstuff.dev/graphql") + ACCOUNTS + @join__graph( + name: "accounts" + url: "http://accounts.demo.starstuff.dev/graphql" + ) + INVENTORY + @join__graph( + name: "inventory" + url: "http://inventory.demo.starstuff.dev/graphql" + ) + PRODUCTS + @join__graph( + name: "products" + url: "http://products.demo.starstuff.dev/graphql" + ) + REVIEWS + @join__graph( + name: "reviews" + url: "http://reviews.demo.starstuff.dev/graphql" + ) +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION } type Mutation { createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) } -type Product @join__owner(graph: PRODUCTS) @join__type(graph: PRODUCTS, key: "upc") @join__type(graph: REVIEWS, key: "upc") @join__type(graph: INVENTORY, key: "upc") { - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible +type Product + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") + @join__type(graph: INVENTORY, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) @@ -49,14 +128,16 @@ type Query { topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review @join__owner(graph: REVIEWS) @join__type(graph: REVIEWS, key: "id") { +type Review @join__type(graph: REVIEWS, key: "id") { author: User @join__field(graph: REVIEWS, provides: "username") body: String @join__field(graph: REVIEWS) id: ID! @join__field(graph: REVIEWS) product: Product @join__field(graph: REVIEWS) } -type User @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") @join__type(graph: REVIEWS, key: "id") { +type User + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { id: ID! @join__field(graph: ACCOUNTS) name: String @join__field(graph: ACCOUNTS) reviews: [Review] @join__field(graph: REVIEWS) diff --git a/apollo-router/src/testdata/inaccessible_on_non_core.graphql b/apollo-router/src/testdata/inaccessible_on_non_core.graphql index 3f8c8bcb24..a43a836c35 100644 --- a/apollo-router/src/testdata/inaccessible_on_non_core.graphql +++ b/apollo-router/src/testdata/inaccessible_on_non_core.graphql @@ -1,26 +1,60 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2") - @core(feature: "https://specs.apollo.dev/join/v0.2", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) - -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) { query: Query } -directive @core(feature: String!, as: String, for: core__Purpose) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph!, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA -directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR +scalar join__FieldSet directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION -enum core__Purpose { +enum join__Graph { + SUBGRAPH @join__graph(name: "users", url: "http://localhost:4001") +} + +scalar link__Import + +enum link__Purpose { """ `SECURITY` features provide metadata necessary to securely resolve fields. """ @@ -32,13 +66,6 @@ enum core__Purpose { EXECUTION } -scalar join__FieldSet - -enum join__Graph { - SUBGRAPH @join__graph(name: "users", url: "http://localhost:4001") -} - - input InputObject { someField: String privateField: String @inaccessible diff --git a/apollo-router/src/testdata/invalid_minimal_supergraph_missing_subgraph_url.graphql b/apollo-router/src/testdata/invalid_minimal_supergraph_missing_subgraph_url.graphql new file mode 100644 index 0000000000..fd4178d111 --- /dev/null +++ b/apollo-router/src/testdata/invalid_minimal_supergraph_missing_subgraph_url.graphql @@ -0,0 +1,31 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query +} + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA +scalar link__Import + +enum join__Graph { + ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001/graphql") + INVENTORY + @join__graph(name: "inventory", url: "http://localhost:4002/graphql") + PRODUCTS @join__graph(name: "products", url: "http://localhost:4003/graphql") + REVIEWS @join__graph(name: "reviews", url: "") +} + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Query { + me: String +} diff --git a/apollo-router/src/testdata/invalid_supergraph.graphql b/apollo-router/src/testdata/invalid_supergraph.graphql index 38b1048051..e2a51efb7e 100644 --- a/apollo-router/src/testdata/invalid_supergraph.graphql +++ b/apollo-router/src/testdata/invalid_supergraph.graphql @@ -1,65 +1,118 @@ # This supergraph is almost valid. the only difference is that the reviews field in the Product type joins on PRODUCTS which is incorrect -schema @core(feature: "https://specs.apollo.dev/core/v0.1") @core(feature: "https://specs.apollo.dev/join/v0.1") @core(feature: "https://specs.apollo.dev/tag/v0.1") @apollo_studio_metadata(launchId: "2396d4fb-a1e4-457d-8da4-347479b852f1", buildId: "2396d4fb-a1e4-457d-8da4-347479b852f1", checkId: null) @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") { +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/tag/v0.3") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA - -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION - -directive @apollo_studio_metadata(launchId: String, buildId: String, checkId: String) on SCHEMA - -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION + +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +scalar link__Import scalar join__FieldSet enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001/graphql") - INVENTORY @join__graph(name: "inventory", url: "http://localhost:4004/graphql") - PRODUCTS @join__graph(name: "products", url: "http://localhost:4003/graphql") - REVIEWS @join__graph(name: "reviews", url: "http://localhost:4002/graphql") + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +enum link__Purpose { + SECURITY + EXECUTION } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } -type Product @join__owner(graph: PRODUCTS) @join__type(graph: PRODUCTS, key: "upc") @join__type(graph: REVIEWS, key: "upc") @join__type(graph: INVENTORY, key: "upc") { - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible +type Product + @join__type(graph: ACCOUNTS, key: "upc", extension: true) + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) price: Int @join__field(graph: PRODUCTS) - reviews: [Review] @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: PRODUCTS) # This is deliberately incorrect reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") upc: String! @join__field(graph: PRODUCTS) weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review @join__owner(graph: REVIEWS) @join__type(graph: REVIEWS, key: "id") { +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! author: User @join__field(graph: REVIEWS, provides: "username") body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) product: Product @join__field(graph: REVIEWS) } -type User @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") @join__type(graph: REVIEWS, key: "id") { - id: ID! @join__field(graph: ACCOUNTS) +type User + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) reviews: [Review] @join__field(graph: REVIEWS) username: String @join__field(graph: ACCOUNTS) diff --git a/apollo-router/src/testdata/minimal_fed1_supergraph.graphql b/apollo-router/src/testdata/minimal_fed1_supergraph.graphql new file mode 100644 index 0000000000..7df337ea43 --- /dev/null +++ b/apollo-router/src/testdata/minimal_fed1_supergraph.graphql @@ -0,0 +1,32 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.apollo.dev/join/v0.1") { + query: Query +} + +directive @core(feature: String!) repeatable on SCHEMA + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet +) on FIELD_DEFINITION + +directive @join__type( + graph: join__Graph! + key: join__FieldSet +) repeatable on OBJECT | INTERFACE + +directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +scalar join__FieldSet + +enum join__Graph { + ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001/graphql") +} + +type Query { + me: String +} diff --git a/apollo-router/src/testdata/minimal_local_inventory_supergraph.graphql b/apollo-router/src/testdata/minimal_local_inventory_supergraph.graphql new file mode 100644 index 0000000000..9cf20a6f0d --- /dev/null +++ b/apollo-router/src/testdata/minimal_local_inventory_supergraph.graphql @@ -0,0 +1,36 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query +} + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA +scalar link__Import + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +enum join__Graph { + ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001/graphql") + INVENTORY + @join__graph(name: "inventory", url: "http://localhost:4002/graphql") + PRODUCTS @join__graph(name: "products", url: "http://localhost:4003/graphql") + REVIEWS @join__graph(name: "reviews", url: "http://localhost:4004/graphql") +} + +enum link__Purpose { + SECURITY + EXECUTION +} +type Query { + me: String +} diff --git a/apollo-router/src/testdata/minimal_supergraph.graphql b/apollo-router/src/testdata/minimal_supergraph.graphql index 7df337ea43..ac0b1860af 100644 --- a/apollo-router/src/testdata/minimal_supergraph.graphql +++ b/apollo-router/src/testdata/minimal_supergraph.graphql @@ -1,32 +1,57 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") { + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query } -directive @core(feature: String!) repeatable on SCHEMA - directive @join__field( graph: join__Graph requires: join__FieldSet provides: join__FieldSet -) on FIELD_DEFINITION + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE directive @join__type( graph: join__Graph! key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +directive @join__implements( + graph: join__Graph! + interface: String! ) repeatable on OBJECT | INTERFACE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE - -directive @join__graph(name: String!, url: String!) on ENUM_VALUE - scalar join__FieldSet +scalar link__Import enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001/graphql") + SUBGRAPH_A + @join__graph( + name: "subgraph-a" + url: "http://graphql.subgraph-a.svc.cluster.local:4000" + ) +} + +enum link__Purpose { + SECURITY + EXECUTION } -type Query { - me: String +type Query @join__type(graph: SUBGRAPH_A) { + me: String @join__field(graph: SUBGRAPH_A) } diff --git a/apollo-router/src/testdata/orga_supergraph.graphql b/apollo-router/src/testdata/orga_supergraph.graphql new file mode 100644 index 0000000000..a6ddb679d0 --- /dev/null +++ b/apollo-router/src/testdata/orga_supergraph.graphql @@ -0,0 +1,79 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query + subscription: Subscription +} + +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION +directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar join__FieldSet +scalar link__Import + +enum join__Graph { + USER @join__graph(name: "user", url: "http://localhost:4001/graphql") + ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") +} + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Query @join__type(graph: USER) @join__type(graph: ORGA) { + currentUser: User @join__field(graph: USER) + otherUser: User @join__field(graph: USER) + orga(id: ID): Organization @join__field(graph: ORGA) +} + +type Subscription @join__type(graph: USER) { + userWasCreated: User +} + +type User + @join__type(graph: ORGA, key: "id") + @join__type(graph: USER, key: "id") { + id: ID! + name: String @join__field(graph: USER) + phone: String @join__field(graph: USER) + activeOrganization: Organization @join__field(graph: USER) + allOrganizations: [Organization] @join__field(graph: USER) +} + +type Organization + @join__type(graph: ORGA, key: "id") + @join__type(graph: USER, key: "id") { + id: ID + creatorUser: User @join__field(graph: ORGA) + name: String @join__field(graph: ORGA) + nonNullId: ID! @join__field(graph: ORGA) + suborga: [Organization] @join__field(graph: ORGA) +} diff --git a/apollo-router/src/testdata/starstuff@current.graphql b/apollo-router/src/testdata/starstuff@current.graphql index abe3698edf..ff57927866 100644 --- a/apollo-router/src/testdata/starstuff@current.graphql +++ b/apollo-router/src/testdata/starstuff@current.graphql @@ -1,62 +1,139 @@ -schema @core(feature: "https://specs.apollo.dev/core/v0.1") @core(feature: "https://specs.apollo.dev/join/v0.1") @core(feature: "https://specs.apollo.dev/tag/v0.1") @apollo_studio_metadata(launchId: "2396d4fb-a1e4-457d-8da4-347479b852f1", buildId: "2396d4fb-a1e4-457d-8da4-347479b852f1", checkId: null) { +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @apollo_studio_metadata( + launchId: "2396d4fb-a1e4-457d-8da4-347479b852f1" + buildId: "2396d4fb-a1e4-457d-8da4-347479b852f1" + checkId: null + ) { query: Query mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "http://accounts.demo.starstuff.dev/graphql") - INVENTORY @join__graph(name: "inventory", url: "http://inventory.demo.starstuff.dev/graphql") - PRODUCTS @join__graph(name: "products", url: "http://products.demo.starstuff.dev/graphql") - REVIEWS @join__graph(name: "reviews", url: "http://reviews.demo.starstuff.dev/graphql") + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION } -type Product @join__owner(graph: PRODUCTS) @join__type(graph: PRODUCTS, key: "upc") @join__type(graph: REVIEWS, key: "upc") @join__type(graph: INVENTORY, key: "upc") { - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) +} + +type Product + @join__type(graph: ACCOUNTS, key: "upc", extension: true) + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) + recommendedProducts: [Product] @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review @join__owner(graph: REVIEWS) @join__type(graph: REVIEWS, key: "id") { +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + product: Product } -type User @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") @join__type(graph: REVIEWS, key: "id") { - id: ID! @join__field(graph: ACCOUNTS) +type User + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } -directive @apollo_studio_metadata(launchId: String, buildId: String, checkId: String) on SCHEMA +directive @apollo_studio_metadata( + launchId: String + buildId: String + checkId: String +) on SCHEMA diff --git a/apollo-router/src/testdata/supergraph.graphql b/apollo-router/src/testdata/supergraph.graphql index 9a64dd4358..c23dd320ca 100644 --- a/apollo-router/src/testdata/supergraph.graphql +++ b/apollo-router/src/testdata/supergraph.graphql @@ -1,66 +1,111 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "https://accounts.demo.starstuff.dev/") - INVENTORY @join__graph(name: "inventory" url: "https://inventory.demo.starstuff.dev/") - PRODUCTS @join__graph(name: "products" url: "https://products.demo.starstuff.dev/") - REVIEWS @join__graph(name: "reviews" url: "https://reviews.demo.starstuff.dev/") + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +scalar link__Import + +enum link__Purpose { + SECURITY + EXECUTION } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - upc: String! @join__field(graph: PRODUCTS) - name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + name: String @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: REVIEWS) - body: String @join__field(graph: REVIEWS) +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") product: Product @join__field(graph: REVIEWS) } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) - username: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) } diff --git a/apollo-router/src/testdata/supergraph_missing_name.graphql b/apollo-router/src/testdata/supergraph_missing_name.graphql index 6b21b4f4e7..91d122492e 100644 --- a/apollo-router/src/testdata/supergraph_missing_name.graphql +++ b/apollo-router/src/testdata/supergraph_missing_name.graphql @@ -1,65 +1,127 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query + mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "https://accounts.demo.starstuff.dev/") - INVENTORY @join__graph(name: "inventory" url: "https://inventory.demo.starstuff.dev/") - PRODUCTS @join__graph(name: "products" url: "https://products.demo.starstuff.dev/") - REVIEWS @join__graph(name: "reviews" url: "https://reviews.demo.starstuff.dev/") + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - upc: String! @join__field(graph: PRODUCTS) - name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + name: String @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) + reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) + recommendedProducts: [Product] @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: REVIEWS) - body: String @join__field(graph: REVIEWS) +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") - product: Product @join__field(graph: REVIEWS) + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) - username: String @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) } diff --git a/apollo-router/src/uplink/testdata/oss.graphql b/apollo-router/src/uplink/testdata/oss.graphql index 7df337ea43..d7c87efa71 100644 --- a/apollo-router/src/uplink/testdata/oss.graphql +++ b/apollo-router/src/uplink/testdata/oss.graphql @@ -1,30 +1,70 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") { + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE directive @join__field( graph: join__Graph requires: join__FieldSet provides: join__FieldSet -) on FIELD_DEFINITION + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE directive @join__type( graph: join__Graph! key: join__FieldSet -) repeatable on OBJECT | INTERFACE + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet enum join__Graph { ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001/graphql") + INVENTORY + @join__graph(name: "inventory", url: "http://localhost:4002/graphql") + PRODUCTS @join__graph(name: "products", url: "http://localhost:4003/graphql") + REVIEWS @join__graph(name: "reviews", url: "http://localhost:4004/graphql") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION } type Query { diff --git a/apollo-router/testing_schema.graphql b/apollo-router/testing_schema.graphql index f724f9ce2b..ffe2c58c14 100644 --- a/apollo-router/testing_schema.graphql +++ b/apollo-router/testing_schema.graphql @@ -1,79 +1,118 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation subscription: Subscription } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet +scalar link__Import enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "http://localhost:4001") - INVENTORY @join__graph(name: "inventory" url: "http://localhost:4004") - PRODUCTS @join__graph(name: "products" url: "http://localhost:4003") - REVIEWS @join__graph(name: "reviews" url: "http://localhost:4002") + ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001") + INVENTORY @join__graph(name: "inventory", url: "http://localhost:4004") + PRODUCTS @join__graph(name: "products", url: "http://localhost:4003") + REVIEWS @join__graph(name: "reviews", url: "http://localhost:4002") +} + +enum link__Purpose { + SECURITY + EXECUTION } -type Mutation { +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) } -type Subscription { +type Subscription @join__type(graph: ACCOUNTS) @join__type(graph: REVIEWS) { userWasCreated: User @join__field(graph: ACCOUNTS) reviewAdded: Review @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ - author: User @join__field(graph: REVIEWS, provides: "username") +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) product: Product @join__field(graph: REVIEWS) + author: User @join__field(graph: REVIEWS, provides: "username") } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index beccfcdddd..664f8970a6 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -103,6 +103,7 @@ struct TracedResponder { response_template: ResponseTemplate, telemetry: Telemetry, subscriber_subgraph: Dispatch, + subgraph_callback: Option>, } impl Respond for TracedResponder { @@ -112,6 +113,9 @@ impl Respond for TracedResponder { let _context_guard = context.attach(); let span = info_span!("subgraph server"); let _span_guard = span.enter(); + if let Some(callback) = &self.subgraph_callback { + callback(); + } self.response_template.clone() }) } @@ -280,6 +284,7 @@ impl IntegrationTest { supergraph: Option, mut subgraph_overrides: HashMap, log: Option, + subgraph_callback: Option>, ) -> Self { let redis_namespace = Uuid::new_v4().to_string(); let telemetry = telemetry.unwrap_or_default(); @@ -313,6 +318,7 @@ impl IntegrationTest { ResponseTemplate::new(200).set_body_json(json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}))), telemetry: telemetry.clone(), subscriber_subgraph: Self::dispatch(&tracer_provider_subgraph), + subgraph_callback }) .mount(&subgraphs) .await; @@ -517,7 +523,7 @@ impl IntegrationTest { pub fn execute_huge_query( &self, ) -> impl std::future::Future { - self.execute_query_internal(&json!({"query":"query {topProducts{name, name, name, name, name, name, name, name, name, name}}","variables":{}}), None, None) + self.execute_query_internal(&json!({"query":"query {topProducts{name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name}}","variables":{}}), None, None) } #[allow(dead_code)] diff --git a/apollo-router/tests/fixtures/accounts.graphql b/apollo-router/tests/fixtures/accounts.graphql index 6cb47c0d5c..33468356a1 100644 --- a/apollo-router/tests/fixtures/accounts.graphql +++ b/apollo-router/tests/fixtures/accounts.graphql @@ -1,9 +1,20 @@ -extend type Query { +extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@shareable", "@external"] + ) + +type Query { me: User + recommendedProducts: [Product] } type User @key(fields: "id") { id: ID! name: String - username: String + username: String @shareable +} + +extend type Product @key(fields: "upc") { + upc: String! @external } diff --git a/apollo-router/tests/fixtures/demand_control.rhai b/apollo-router/tests/fixtures/demand_control.rhai new file mode 100644 index 0000000000..8232b1ad28 --- /dev/null +++ b/apollo-router/tests/fixtures/demand_control.rhai @@ -0,0 +1,18 @@ +fn supergraph_service(service) { + const response_callback = Fn("process_response"); + service.map_response(response_callback); +} + +fn process_response(response) { + const estimate = response.context[Router.APOLLO_COST_ESTIMATED_KEY]; + response.headers["demand-control-estimate"] = to_string(estimate); + + const actual = response.context[Router.APOLLO_COST_ACTUAL_KEY]; + response.headers["demand-control-actual"] = to_string(actual); + + const strategy = response.context[Router.APOLLO_COST_STRATEGY_KEY]; + response.headers["demand-control-strategy"] = strategy; + + const result = response.context[Router.APOLLO_COST_RESULT_KEY]; + response.headers["demand-control-result"] = result; +} \ No newline at end of file diff --git a/apollo-router/tests/fixtures/inventory.graphql b/apollo-router/tests/fixtures/inventory.graphql index 5db69e4267..b9a58c0fc5 100644 --- a/apollo-router/tests/fixtures/inventory.graphql +++ b/apollo-router/tests/fixtures/inventory.graphql @@ -1,13 +1,13 @@ -directive @tag(name: String!) repeatable on - | FIELD_DEFINITION - | INTERFACE - | OBJECT - | UNION +extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@external", "@requires"] + ) -extend type Product @key(fields: "upc") { - upc: String! @external +type Product @key(fields: "upc") { + upc: String! weight: Int @external price: Int @external - inStock: Boolean @tag(name: "private") + inStock: Boolean shippingEstimate: Int @requires(fields: "price weight") } diff --git a/apollo-router/tests/fixtures/products.graphql b/apollo-router/tests/fixtures/products.graphql index 0b575c1f9b..8cd5747f1d 100644 --- a/apollo-router/tests/fixtures/products.graphql +++ b/apollo-router/tests/fixtures/products.graphql @@ -1,4 +1,11 @@ -extend type Mutation { +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + +type Query { + topProducts(first: Int = 5): [Product] +} + +type Mutation { createProduct(upc: ID!, name: String): Product } @@ -8,7 +15,3 @@ type Product @key(fields: "upc") { price: Int weight: Int } - -extend type Query { - topProducts(first: Int = 5): [Product] -} diff --git a/apollo-router/tests/fixtures/reviews.graphql b/apollo-router/tests/fixtures/reviews.graphql index e42ed6fbca..46254285c4 100644 --- a/apollo-router/tests/fixtures/reviews.graphql +++ b/apollo-router/tests/fixtures/reviews.graphql @@ -1,11 +1,11 @@ -extend type Mutation { - createReview(upc: ID!, id: ID!, body: String): Review -} +extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@external", "@provides"] + ) -extend type Product @key(fields: "upc") { - upc: String! @external - reviews: [Review] - reviewsForAuthor(authorID: ID!): [Review] +type Mutation { + createReview(upc: ID!, id: ID!, body: String): Review } type Review @key(fields: "id") { @@ -15,8 +15,14 @@ type Review @key(fields: "id") { product: Product } -extend type User @key(fields: "id") { - id: ID! @external +type User @key(fields: "id") { + id: ID! username: String @external reviews: [Review] } + +type Product @key(fields: "upc") { + upc: String! + reviews: [Review] + reviewsForAuthor(authorID: ID!): [Review] +} diff --git a/apollo-router/tests/fixtures/set_context/one.json b/apollo-router/tests/fixtures/set_context/one.json index e552a0f47b..cde9637ea5 100644 --- a/apollo-router/tests/fixtures/set_context/one.json +++ b/apollo-router/tests/fixtures/set_context/one.json @@ -19,25 +19,6 @@ } } }, - { - "request": { - "query": "query Query__Subgraph1__0{t{__typename prop id u{__typename id}}}", - "operationName": "Query__Subgraph1__0" - }, - "response": { - "data": { - "t": { - "__typename": "T", - "prop": "prop value", - "id": "1", - "u": { - "__typename": "U", - "id": "1" - } - } - } - } - }, { "request": { "query": "query Query__Subgraph1__0{t{__typename prop id uList{__typename id}}}", @@ -140,27 +121,6 @@ } } }, - { - "request": { - "query": "query Query__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", - "operationName": "Query__Subgraph1__1", - "variables": { - "contextualArgument_1_0": "prop value", - "representations": [{ "__typename": "U", "id": "1" }] - } - }, - "response": { - "data": { - "_entities": [ - { - "__typename": "U", - "id": "1", - "field": 1234 - } - ] - } - } - }, { "request": { "query": "query Query__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", @@ -269,45 +229,6 @@ } } }, - { - "request": { - "query": "query Query_Null_Param__Subgraph1__0{t{__typename prop id u{__typename id}}}", - "operationName": "Query_Null_Param__Subgraph1__0" - }, - "response": { - "data": { - "t": { - "__typename": "T", - "prop": null, - "id": "1", - "u": { - "__typename": "U", - "id": "1" - } - } - } - } - }, - { - "request": { - "query": "query Query_Null_Param__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", - "operationName": "Query_Null_Param__Subgraph1__1", - "variables": { - "contextualArgument_1_0": null, - "representations": [{ "__typename": "U", "id": "1" }] - } - }, - "response": { - "data": { - "_entities": [ - { - "id": "1", - "field": 1234 - } - ] - } - } - }, { "request": { "query": "query Query_type_mismatch__Subgraph1__0{t{__typename prop id u{__typename id}}}", @@ -346,69 +267,6 @@ ] } } - }, - { - "request": { - "query": "query Query_fetch_failure__Subgraph1__0{t{__typename prop id u{__typename id}}}", - "operationName": "Query_fetch_failure__Subgraph1__0" - }, - "response": { - "data": { - "t": { - "__typename": "T", - "prop": "prop value", - "id": "1", - "u": { - "__typename": "U", - "id": "1" - } - } - } - } - }, - { - "request": { - "query": "query Query_fetch_failure__Subgraph1__2($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", - "operationName": "Query_fetch_failure__Subgraph1__2", - "variables": { - "contextualArgument_1_0": "prop value", - "representations": [{ "__typename": "U", "id": "1" }] - } - }, - "response": { - "data": { - "t": { - "__typename": "T", - "prop": "prop value", - "id": "1", - "u": { - "__typename": "U", - "id": "1" - } - } - } - } - }, - { - "request": { - "query": "query Query_fetch_dependent_failure__Subgraph1__0{t{__typename prop id u{__typename id}}}", - "operationName": "Query_fetch_dependent_failure__Subgraph1__0" - }, - "response": { - "response": { - "data": null, - "errors": [{ - "message": "Some error", - "locations": [ - { - "line": 3, - "column": 5 - } - ], - "path": ["t", "u"] - }] - } - } } ] } diff --git a/apollo-router/tests/fixtures/set_context/one_dependent_fetch_failure.json b/apollo-router/tests/fixtures/set_context/one_dependent_fetch_failure.json new file mode 100644 index 0000000000..9dd16bd60f --- /dev/null +++ b/apollo-router/tests/fixtures/set_context/one_dependent_fetch_failure.json @@ -0,0 +1,23 @@ +{ + "mocks": [ + { + "request": { + "query": "query Query_fetch_dependent_failure__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query_fetch_dependent_failure__Subgraph1__0" + }, + "response": { + "data": null, + "errors": [{ + "message": "Some error", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": ["t", "u"] + }] + } + } + ] +} \ No newline at end of file diff --git a/apollo-router/tests/fixtures/set_context/one_fetch_failure.json b/apollo-router/tests/fixtures/set_context/one_fetch_failure.json new file mode 100644 index 0000000000..5515102f2d --- /dev/null +++ b/apollo-router/tests/fixtures/set_context/one_fetch_failure.json @@ -0,0 +1,46 @@ +{ + "mocks": [ + { + "request": { + "query": "query Query_fetch_failure__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query_fetch_failure__Subgraph1__0" + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": "prop value", + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } + }, + { + "request": { + "query": "query Query_fetch_failure__Subgraph1__2($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query_fetch_failure__Subgraph1__2", + "variables": { + "contextualArgument_1_0": "prop value", + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": "prop value", + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/apollo-router/tests/fixtures/set_context/one_null_param.json b/apollo-router/tests/fixtures/set_context/one_null_param.json new file mode 100644 index 0000000000..f36994a1a0 --- /dev/null +++ b/apollo-router/tests/fixtures/set_context/one_null_param.json @@ -0,0 +1,43 @@ +{ + "mocks": [ + { + "request": { + "query": "query Query_Null_Param__Subgraph1__0{t{__typename prop id u{__typename id}}}", + "operationName": "Query_Null_Param__Subgraph1__0" + }, + "response": { + "data": { + "t": { + "__typename": "T", + "prop": null, + "id": "1", + "u": { + "__typename": "U", + "id": "1" + } + } + } + } + }, + { + "request": { + "query": "query Query_Null_Param__Subgraph1__1($representations:[_Any!]!$contextualArgument_1_0:String){_entities(representations:$representations){...on U{field(a:$contextualArgument_1_0)}}}", + "operationName": "Query_Null_Param__Subgraph1__1", + "variables": { + "contextualArgument_1_0": null, + "representations": [{ "__typename": "U", "id": "1" }] + } + }, + "response": { + "data": { + "_entities": [ + { + "id": "1", + "field": 1234 + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/apollo-router/tests/fixtures/supergraph.graphql b/apollo-router/tests/fixtures/supergraph.graphql index 971174056a..47036a804c 100644 --- a/apollo-router/tests/fixtures/supergraph.graphql +++ b/apollo-router/tests/fixtures/supergraph.graphql @@ -1,90 +1,131 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA - -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION +scalar join__FieldSet -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +scalar link__Import +enum link__Purpose { """ `SECURITY` features provide metadata necessary to securely resolve fields. """ SECURITY -} -scalar join__FieldSet - -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + inStock: Boolean @join__field(graph: INVENTORY) @inaccessible + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) + recommendedProducts: [Product] @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/integration/coprocessor.rs b/apollo-router/tests/integration/coprocessor.rs index 213947eae4..d1492fbc87 100644 --- a/apollo-router/tests/integration/coprocessor.rs +++ b/apollo-router/tests/integration/coprocessor.rs @@ -82,3 +82,71 @@ async fn test_coprocessor_limit_payload() -> Result<(), BoxError> { router.graceful_shutdown().await; Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn test_coprocessor_demand_control_access() -> Result<(), BoxError> { + if !graph_os_enabled() { + return Ok(()); + } + + let mock_server = wiremock::MockServer::start().await; + let coprocessor_address = mock_server.uri(); + + // Assert the execution request stage has access to the estimated cost + Mock::given(method("POST")) + .and(path("/")) + .and(body_partial_json(json!({ + "stage": "ExecutionRequest", + "context": { + "entries": { + "cost.estimated": 10.0, + "cost.result": "COST_OK", + "cost.strategy": "static_estimated" + }}}))) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "version":1, + "stage":"ExecutionRequest", + "control":"continue", + }))) + .expect(1) + .mount(&mock_server) + .await; + + // Assert the supergraph response stage also includes the actual cost + Mock::given(method("POST")) + .and(path("/")) + .and(body_partial_json(json!({ + "stage": "SupergraphResponse", + "context": {"entries": { + "cost.actual": 3.0, + "cost.estimated": 10.0, + "cost.result": "COST_OK", + "cost.strategy": "static_estimated" + }}}))) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "version":1, + "stage":"SupergraphResponse", + "control":"continue", + }))) + .expect(1) + .mount(&mock_server) + .await; + + let mut router = IntegrationTest::builder() + .config( + include_str!("fixtures/coprocessor_demand_control.router.yaml") + .replace("", &coprocessor_address), + ) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let (_trace_id, response) = router.execute_default_query().await; + assert_eq!(response.status(), 200); + + router.graceful_shutdown().await; + + Ok(()) +} diff --git a/apollo-router/tests/integration/fixtures/coprocessor_demand_control.router.yaml b/apollo-router/tests/integration/fixtures/coprocessor_demand_control.router.yaml new file mode 100644 index 0000000000..6535dd92bf --- /dev/null +++ b/apollo-router/tests/integration/fixtures/coprocessor_demand_control.router.yaml @@ -0,0 +1,17 @@ +# This coprocessor url will be updated to a test-scoped mock server +coprocessor: + url: "" + execution: + request: + context: true + supergraph: + response: + context: true + +demand_control: + enabled: true + mode: measure + strategy: + static_estimated: + list_size: 10 + max: 1000 \ No newline at end of file diff --git a/apollo-router/tests/integration/introspection.rs b/apollo-router/tests/integration/introspection.rs index 38fe62a70d..64aea564d5 100644 --- a/apollo-router/tests/integration/introspection.rs +++ b/apollo-router/tests/integration/introspection.rs @@ -233,7 +233,7 @@ async fn both_mode_integration() { let mut router = IntegrationTest::builder() .config( " - experimental_introspection_mode: both + # `experimental_introspection_mode` now defaults to `both` supergraph: introspection: true ", @@ -249,8 +249,6 @@ async fn both_mode_integration() { "query": include_str!("../fixtures/introspect_full_schema.graphql"), })) .await; - // TODO: should be a match after https://apollographql.atlassian.net/browse/ROUTER-703 - // router.assert_log_contains("Introspection match! 🎉").await; - router.assert_log_contains("Introspection mismatch").await; + router.assert_log_contains("Introspection match! 🎉").await; router.graceful_shutdown().await; } diff --git a/apollo-router/tests/integration/query_planner.rs b/apollo-router/tests/integration/query_planner.rs index 9c85c99690..03056ab47d 100644 --- a/apollo-router/tests/integration/query_planner.rs +++ b/apollo-router/tests/integration/query_planner.rs @@ -13,7 +13,7 @@ const BOTH_BEST_EFFORT_QP: &str = "experimental_query_planner_mode: both_best_ef async fn fed1_schema_with_legacy_qp() { let mut router = IntegrationTest::builder() .config(LEGACY_QP) - .supergraph("../examples/graphql/local.graphql") + .supergraph("../examples/graphql/supergraph-fed1.graphql") .build() .await; router.start().await; @@ -26,7 +26,7 @@ async fn fed1_schema_with_legacy_qp() { async fn fed1_schema_with_new_qp() { let mut router = IntegrationTest::builder() .config(NEW_QP) - .supergraph("../examples/graphql/local.graphql") + .supergraph("../examples/graphql/supergraph-fed1.graphql") .build() .await; router.start().await; @@ -44,7 +44,7 @@ async fn fed1_schema_with_new_qp() { async fn fed1_schema_with_both_qp() { let mut router = IntegrationTest::builder() .config(BOTH_QP) - .supergraph("../examples/graphql/local.graphql") + .supergraph("../examples/graphql/supergraph-fed1.graphql") .build() .await; router.start().await; @@ -62,7 +62,7 @@ async fn fed1_schema_with_both_qp() { async fn fed1_schema_with_both_best_effort_qp() { let mut router = IntegrationTest::builder() .config(BOTH_BEST_EFFORT_QP) - .supergraph("../examples/graphql/local.graphql") + .supergraph("../examples/graphql/supergraph-fed1.graphql") .build() .await; router.start().await; @@ -84,7 +84,7 @@ async fn fed1_schema_with_legacy_qp_reload_to_new_keep_previous_config() { let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); let mut router = IntegrationTest::builder() .config(config) - .supergraph("../examples/graphql/local.graphql") + .supergraph("../examples/graphql/supergraph-fed1.graphql") .build() .await; router.start().await; @@ -111,7 +111,7 @@ async fn fed1_schema_with_legacy_qp_reload_to_both_best_effort_keep_previous_con let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); let mut router = IntegrationTest::builder() .config(config) - .supergraph("../examples/graphql/local.graphql") + .supergraph("../examples/graphql/supergraph-fed1.graphql") .build() .await; router.start().await; @@ -143,7 +143,7 @@ async fn fed2_schema_with_new_qp() { let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{NEW_QP}"); let mut router = IntegrationTest::builder() .config(config) - .supergraph("../examples/graphql/supergraph-fed2.graphql") + .supergraph("../examples/graphql/supergraph.graphql") .build() .await; router.start().await; @@ -158,112 +158,6 @@ async fn fed2_schema_with_new_qp() { router.graceful_shutdown().await; } -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_legacy_qp() { - if !graph_os_enabled() { - return; - } - let mut router = IntegrationTest::builder() - .config(LEGACY_QP) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_new_qp() { - if !graph_os_enabled() { - return; - } - let mut router = IntegrationTest::builder() - .config(NEW_QP) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router - .assert_log_contains( - "could not create router: \ - failed to initialize the query planner: \ - `experimental_query_planner_mode: new` or `both` cannot yet \ - be used with progressive overrides. \ - Remove uses of progressive overrides to try the experimental query planner, \ - otherwise switch back to `legacy` or `both_best_effort`.", - ) - .await; - router.assert_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_legacy_qp_change_to_new_qp_keeps_old_config() { - if !graph_os_enabled() { - return; - } - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); - let mut router = IntegrationTest::builder() - .config(config) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{NEW_QP}"); - router.update_config(&config).await; - router - .assert_log_contains("error while reloading, continuing with previous configuration") - .await; - router - .assert_metrics_contains( - r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="overrides",init_is_success="false",otel_scope_name="apollo/router"} 1"#, - None, - ) - .await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_legacy_qp_reload_to_both_best_effort_keep_previous_config() { - if !graph_os_enabled() { - return; - } - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); - let mut router = IntegrationTest::builder() - .config(config) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{BOTH_BEST_EFFORT_QP}"); - router.update_config(&config).await; - router - .assert_log_contains( - "Falling back to the legacy query planner: \ - failed to initialize the query planner: \ - `experimental_query_planner_mode: new` or `both` cannot yet \ - be used with progressive overrides. \ - Remove uses of progressive overrides to try the experimental query planner, \ - otherwise switch back to `legacy` or `both_best_effort`.", - ) - .await; - router - .assert_metrics_contains( - r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="overrides",init_is_success="false",otel_scope_name="apollo/router"} 1"#, - None, - ) - .await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - #[tokio::test(flavor = "multi_thread")] async fn context_with_legacy_qp() { if !graph_os_enabled() { diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index f95ba37c65..cbd1f5dc0a 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -1,3 +1,28 @@ +// The redis cache keys in this file have to change whenever the following change: +// * the supergraph schema +// * experimental_query_planner_mode +// * federation version +// +// How to get the new cache key: +// If you have redis running locally, you can skip step 1 and proceed with steps 2-3. +// 1. run `docker-compose up -d` and connect to the redis container by running `docker-compose exec redis /bin/bash`. +// 2. Run the `redis-cli` command from the shell and start the redis `monitor` command. +// 3. Run the failing test and yank the updated cache key from the redis logs. It will be the key following `SET`, for example: +// ```bash +// 1724831727.472732 [0 127.0.0.1:56720] "SET" +// "plan:0:v2.8.5:70f115ebba5991355c17f4f56ba25bb093c519c4db49a30f3b10de279a4e3fa4:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:4f9f0183101b2f249a364b98adadfda6e5e2001d1f2465c988428cf1ac0b545f" +// "{\"Ok\":{\"Plan\":{\"plan\":{\"usage_reporting\":{\"statsReportKey\":\"# +// -\\n{topProducts{name +// name}}\",\"referencedFieldsByType\":{\"Product\":{\"fieldNames\":[\"name\"],\"isInterface\":false},\"Query\":{\"fieldNames\":[\"topProducts\"],\"isInterface\":false}}},\"root\":{\"kind\":\"Fetch\",\"serviceName\":\"products\",\"variableUsages\":[],\"operation\":\"{topProducts{name +// name2:name}}\",\"operationName\":null,\"operationKind\":\"query\",\"id\":null,\"inputRewrites\":null,\"outputRewrites\":null,\"contextRewrites\":null,\"schemaAwareHash\":\"121b9859eba2d8fa6dde0a54b6e3781274cf69f7ffb0af912e92c01c6bfff6ca\",\"authorization\":{\"is_authenticated\":false,\"scopes\":[],\"policies\":[]}},\"formatted_query_plan\":\"QueryPlan +// {\\n Fetch(service: \\\"products\\\") {\\n {\\n topProducts {\\n +// name\\n name2: name\\n }\\n }\\n +// n },\\n}\",\"query\":{\"string\":\"{\\n topProducts {\\n name\\n +// name2: name\\n +// }\\n}\\n\",\"fragments\":{\"map\":{}},\"operations\":[{\"name\":null,\"kind\":\"query\",\"type_name\":\"Query\",\"selection_set\":[{\"Field\":{\"name\":\"topProducts\",\"alias\":null,\"selection_set\":[{\"Field\":{\"name\":\"name\",\"alias\":null,\"selection_set\":null,\"field_type\":{\"Named\":\"String\"},\"include_skip\":{\"include\":\"Yes\",\"skip\":\"No\"}}},{\"Field\":{\"name\":\"name\",\"alias\":\"name2\",\"selection_set\":null,\"field_type\":{\"Named\":\"String\"},\"include_skip\":{\"include\":\"Yes\",\"skip\":\"No\"}}}],\"field_type\":{\"List\":{\"Named\":\"Product\"}},\"include_skip\":{\"include\":\"Yes\",\"skip\":\"No\"}}}],\"variables\":{}}],\"subselections\":{},\"unauthorized\":{\"paths\":[],\"errors\":{\"log\":true,\"response\":\"errors\"}},\"filtered_query\":null,\"defer_stats\":{\"has_defer\":false,\"has_unconditional_defer\":false,\"conditional_defer_variable_names\":[]},\"is_original\":true,\"schema_aware_hash\":[20,152,93,92,189,0,240,140,9,65,84,255,4,76,202,231,69,183,58,121,37,240,0,109,198,125,1,82,12,42,179,189]},\"query_metrics\":{\"depth\":2,\"height\":3,\"root_fields\":1,\"aliases\":1},\"estimated_size\":0}}}}" +// "EX" "10" +// ``` + use apollo_router::plugin::test::MockSubgraph; use apollo_router::services::router; use apollo_router::services::supergraph; @@ -22,11 +47,8 @@ use crate::integration::IntegrationTest; #[tokio::test(flavor = "multi_thread")] async fn query_planner_cache() -> Result<(), BoxError> { // If this test fails and the cache key format changed you'll need to update the key here. - // 1. Force this test to run locally by removing the cfg() line at the top of this file. - // 2. run `docker compose up -d` and connect to the redis container by running `docker-compose exec redis /bin/bash`. - // 3. Run the `redis-cli` command from the shell and start the redis `monitor` command. - // 4. Run this test and yank the updated cache key from the redis logs. - let known_cache_key = "plan:0:v2.9.0:16385ebef77959fcdc520ad507eb1f7f7df28f1d54a0569e3adabcb4cd00d7ce:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:8ecc6cbc98bab2769e6666a72ba47a4ebd90e6f62256ddcbdc7f352a805e0fe6"; + // Look at the top of the file for instructions on getting the new cache key. + let known_cache_key = "plan:0:v2.9.1:70f115ebba5991355c17f4f56ba25bb093c519c4db49a30f3b10de279a4e3fa4:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:4f9f0183101b2f249a364b98adadfda6e5e2001d1f2465c988428cf1ac0b545f"; let config = RedisConfig::from_url("redis://127.0.0.1:6379").unwrap(); let client = RedisClient::new(config, None, None, None); @@ -369,7 +391,8 @@ async fn entity_cache() -> Result<(), BoxError> { "enabled": false, "redis": { "urls": ["redis://127.0.0.1:6379"], - "ttl": "2s" + "ttl": "2s", + "pool_size": 3 }, }, "subgraphs": { @@ -921,13 +944,13 @@ async fn connection_failure_blocks_startup() { async fn query_planner_redis_update_query_fragments() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_query_fragments.router.yaml"), - "plan:0:v2.9.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:cda2b4e476fdce9c4c435627b26cedd177cfbe04ab335fc3e3d895c0d79d965e", + "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:78f3ccab3def369f4b809a0f8c8f6e90545eb08cd1efeb188ffc663b902c1f2d", ) .await; } #[tokio::test(flavor = "multi_thread")] -#[ignore = "extraction of subgraphs from supergraph is not yet implemented"] +#[ignore = "the cache key for different query planner modes is currently different"] async fn query_planner_redis_update_planner_mode() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_query_planner_mode.router.yaml"), @@ -938,40 +961,84 @@ async fn query_planner_redis_update_planner_mode() { #[tokio::test(flavor = "multi_thread")] async fn query_planner_redis_update_introspection() { + // If this test fails and the cache key format changed you'll need to update + // the key here. Look at the top of the file for instructions on getting + // the new cache key. + // + // You first need to follow the process and update the key in + // `test_redis_query_plan_config_update`, and then update the key in this + // test. + // + // This test requires graphos license, so make sure you have + // "TEST_APOLLO_KEY" and "TEST_APOLLO_GRAPH_REF" env vars set, otherwise the + // test just passes locally. test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_introspection.router.yaml"), - "plan:0:v2.9.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:259dd917e4de09b5469629849b91e8ffdfbed2587041fad68b5963369bb13283", + "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:99a70d6c967eea3bc68721e1094f586f5ae53c7e12f83a650abd5758c372d048", ) .await; } #[tokio::test(flavor = "multi_thread")] async fn query_planner_redis_update_defer() { + // If this test fails and the cache key format changed you'll need to update + // the key here. Look at the top of the file for instructions on getting + // the new cache key. + // + // You first need to follow the process and update the key in + // `test_redis_query_plan_config_update`, and then update the key in this + // test. + // + // This test requires graphos license, so make sure you have + // "TEST_APOLLO_KEY" and "TEST_APOLLO_GRAPH_REF" env vars set, otherwise the + // test just passes locally. test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), - "plan:0:v2.9.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:e4376fe032160ce16399e520c6e815da6cb5cf4dc94a06175b86b64a9bf80201", + "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:d6a3d7807bb94cfb26be4daeb35e974680b53755658fafd4c921c70cec1b7c39", ) .await; } #[tokio::test(flavor = "multi_thread")] async fn query_planner_redis_update_type_conditional_fetching() { + // If this test fails and the cache key format changed you'll need to update + // the key here. Look at the top of the file for instructions on getting + // the new cache key. + // + // You first need to follow the process and update the key in + // `test_redis_query_plan_config_update`, and then update the key in this + // test. + // + // This test requires graphos license, so make sure you have + // "TEST_APOLLO_KEY" and "TEST_APOLLO_GRAPH_REF" env vars set, otherwise the + // test just passes locally. test_redis_query_plan_config_update( include_str!( "fixtures/query_planner_redis_config_update_type_conditional_fetching.router.yaml" ), - "plan:0:v2.9.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:83d899fcb42d2202c39fc8350289b8247021da00ecf3d844553c190c49410507", + "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:8991411cc7b66d9f62ab1e661f2ce9ccaf53b0d203a275e43ced9a8b6bba02dd", ) .await; } #[tokio::test(flavor = "multi_thread")] async fn query_planner_redis_update_reuse_query_fragments() { + // If this test fails and the cache key format changed you'll need to update + // the key here. Look at the top of the file for instructions on getting + // the new cache key. + // + // You first need to follow the process and update the key in + // `test_redis_query_plan_config_update`, and then update the key in this + // test. + // + // This test requires graphos license, so make sure you have + // "TEST_APOLLO_KEY" and "TEST_APOLLO_GRAPH_REF" env vars set, otherwise the + // test just passes locally. test_redis_query_plan_config_update( include_str!( "fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml" ), - "plan:0:v2.9.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:d48f92f892bd67071694c0538a7e657ff8e0c52e1718f475190c17b503e9e8c3", + "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:c05e89caeb8efc4e8233e8648099b33414716fe901e714416fd0f65a67867f07", ) .await; } @@ -980,9 +1047,10 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key if !graph_os_enabled() { return; } - // This test shows that the redis key changes when the query planner config changes. - // The test starts a router with a specific config, executes a query, and checks the redis cache key. - // Then it updates the config, executes the query again, and checks the redis cache key. + // This test shows that the redis key changes when the query planner config + // changes. The test starts a router with a specific config, executes a + // query, and checks the redis cache key. Then it updates the config, + // executes the query again, and checks the redis cache key. let mut router = IntegrationTest::builder() .config(include_str!( "fixtures/query_planner_redis_config_update.router.yaml" @@ -994,7 +1062,9 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key router.assert_started().await; router.clear_redis_cache().await; - let starting_key = "plan:0:v2.9.0:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:0966f1528d47cee30b6140a164be16148dd360ee10b87744991e9d35af8e8a27"; + // If the tests above are failing, this is the key that needs to be changed first. + let starting_key = "plan:0:v2.9.1:e15b4f5cd51b8cc728e3f5171611073455601e81196cd3cbafc5610d9769a370:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:a52c81e3e2e47c8363fbcd2653e196431c15716acc51fce4f58d9368ac4c2d8d"; + router.execute_default_query().await; router.assert_redis_cache_contains(starting_key, None).await; router.update_config(updated_config).await; diff --git a/apollo-router/tests/integration/telemetry/datadog.rs b/apollo-router/tests/integration/telemetry/datadog.rs index d2d3f58d25..6aed76ff6d 100644 --- a/apollo-router/tests/integration/telemetry/datadog.rs +++ b/apollo-router/tests/integration/telemetry/datadog.rs @@ -2,13 +2,18 @@ extern crate core; use std::collections::HashMap; use std::collections::HashSet; +use std::sync::atomic::AtomicBool; use std::time::Duration; use anyhow::anyhow; +use opentelemetry_api::trace::TraceContextExt; use opentelemetry_api::trace::TraceId; use serde_json::json; use serde_json::Value; use tower::BoxError; +use tracing::Span; +use tracing_opentelemetry::OpenTelemetrySpanExt; +use wiremock::ResponseTemplate; use crate::integration::common::graph_os_enabled; use crate::integration::common::Telemetry; @@ -25,6 +30,38 @@ struct TraceSpec { unmeasured_spans: HashSet<&'static str>, } +#[tokio::test(flavor = "multi_thread")] +async fn test_no_sample() -> Result<(), BoxError> { + if !graph_os_enabled() { + return Ok(()); + } + let subgraph_was_sampled = std::sync::Arc::new(AtomicBool::new(false)); + let subgraph_was_sampled_callback = subgraph_was_sampled.clone(); + let mut router = IntegrationTest::builder() + .telemetry(Telemetry::Datadog) + .config(include_str!("fixtures/datadog_no_sample.router.yaml")) + .responder(ResponseTemplate::new(200).set_body_json( + json!({"data":{"topProducts":[{"name":"Table"},{"name":"Couch"},{"name":"Chair"}]}}), + )) + .subgraph_callback(Box::new(move || { + let sampled = Span::current().context().span().span_context().is_sampled(); + subgraph_was_sampled_callback.store(sampled, std::sync::atomic::Ordering::SeqCst); + })) + .build() + .await; + + router.start().await; + router.assert_started().await; + + let query = json!({"query":"query ExampleQuery {topProducts{name}}","variables":{}}); + let (_id, result) = router.execute_untraced_query(&query).await; + router.graceful_shutdown().await; + assert!(result.status().is_success()); + assert!(!subgraph_was_sampled.load(std::sync::atomic::Ordering::SeqCst)); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn test_default_span_names() -> Result<(), BoxError> { if !graph_os_enabled() { diff --git a/apollo-router/tests/integration/telemetry/fixtures/datadog_no_sample.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/datadog_no_sample.router.yaml new file mode 100644 index 0000000000..d89d104346 --- /dev/null +++ b/apollo-router/tests/integration/telemetry/fixtures/datadog_no_sample.router.yaml @@ -0,0 +1,24 @@ +telemetry: + apollo: + field_level_instrumentation_sampler: always_off + exporters: + tracing: + experimental_response_trace_id: + enabled: true + header_name: apollo-custom-trace-id + format: datadog + common: + service_name: router + # NOT always_off to allow us to test a sampling probability of zero + sampler: 0.0 + datadog: + enabled: true + batch_processor: + scheduled_delay: 100ms + fixed_span_names: false + enable_span_mapping: false + instrumentation: + spans: + mode: spec_compliant + + diff --git a/apollo-router/tests/integration/telemetry/fixtures/prometheus.router.yaml b/apollo-router/tests/integration/telemetry/fixtures/prometheus.router.yaml index 53c258bb41..1ae02a4dff 100644 --- a/apollo-router/tests/integration/telemetry/fixtures/prometheus.router.yaml +++ b/apollo-router/tests/integration/telemetry/fixtures/prometheus.router.yaml @@ -1,5 +1,5 @@ limits: - http_max_request_bytes: 60 + http_max_request_bytes: 200 telemetry: exporters: metrics: @@ -9,18 +9,18 @@ telemetry: path: /metrics common: views: - - name: apollo_router_http_request_duration_seconds - aggregation: - histogram: - buckets: - - 0.1 - - 0.5 - - 1 - - 2 - - 3 - - 4 - - 5 - - 100 + - name: apollo_router_http_request_duration_seconds + aggregation: + histogram: + buckets: + - 0.1 + - 0.5 + - 1 + - 2 + - 3 + - 4 + - 5 + - 100 attributes: subgraph: all: @@ -39,3 +39,10 @@ override_subgraph_url: products: http://localhost:4005 include_subgraph_errors: all: true +supergraph: + introspection: true +apq: + router: + cache: + in_memory: + limit: 1000 \ No newline at end of file diff --git a/apollo-router/tests/integration/telemetry/metrics.rs b/apollo-router/tests/integration/telemetry/metrics.rs index f6d6e56417..aee5de813e 100644 --- a/apollo-router/tests/integration/telemetry/metrics.rs +++ b/apollo-router/tests/integration/telemetry/metrics.rs @@ -220,3 +220,72 @@ async fn test_graphql_metrics() { .assert_metrics_contains(r#"custom_histogram_sum{graphql_field_name="topProducts",graphql_field_type="Product",graphql_type_name="Query",otel_scope_name="apollo/router"} 3"#, None) .await; } + +#[tokio::test(flavor = "multi_thread")] +async fn test_gauges_on_reload() { + let mut router = IntegrationTest::builder() + .config(include_str!("fixtures/no-telemetry.router.yaml")) + .build() + .await; + + router.start().await; + router.assert_started().await; + router.execute_default_query().await; + router.update_config(PROMETHEUS_CONFIG).await; + router.assert_reloaded().await; + + // Regular query + router.execute_default_query().await; + + // Introspection query + router + .execute_query(&json!({"query":"{__schema {types {name}}}","variables":{}})) + .await; + + // Persisted query + router + .execute_query( + &json!({"query": "{__typename}", "variables":{}, "extensions": {"persistedQuery":{"version" : 1, "sha256Hash" : "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}}) + ) + .await; + + router + .assert_metrics_contains(r#"apollo_router_cache_storage_estimated_size{kind="query planner",type="memory",otel_scope_name="apollo/router"} "#, None) + .await; + router + .assert_metrics_contains( + r#"apollo_router_query_planning_queued{otel_scope_name="apollo/router"} "#, + None, + ) + .await; + router + .assert_metrics_contains( + r#"apollo_router_v8_heap_total_bytes{otel_scope_name="apollo/router"} "#, + None, + ) + .await; + router + .assert_metrics_contains( + r#"apollo_router_v8_heap_total_bytes{otel_scope_name="apollo/router"} "#, + None, + ) + .await; + router + .assert_metrics_contains( + r#"apollo_router_cache_size{kind="APQ",type="memory",otel_scope_name="apollo/router"} 1"#, + None, + ) + .await; + router + .assert_metrics_contains( + r#"apollo_router_cache_size{kind="query planner",type="memory",otel_scope_name="apollo/router"} 3"#, + None, + ) + .await; + router + .assert_metrics_contains( + r#"apollo_router_cache_size{kind="introspection",type="memory",otel_scope_name="apollo/router"} 1"#, + None, + ) + .await; +} diff --git a/apollo-router/tests/samples/README.md b/apollo-router/tests/samples/README.md index 9f1aa78f0d..c37c65e06b 100644 --- a/apollo-router/tests/samples/README.md +++ b/apollo-router/tests/samples/README.md @@ -1,6 +1,6 @@ # File based integration tests -This folder contains a serie of Router integration tests that can be defined entirely through a JSON file. Thos tests are able to start and stop a router, reload its schema or configiration, make requests and check the expected response. While we can make similar tests from inside the Router's code, these tests here are faster to write and modify because they do not require recompilations of the Router, at the cost of a slightly higher runtime cost. +This folder contains a series of Router integration tests that can be defined entirely through a JSON file. Thos tests are able to start and stop a router, reload its schema or configiration, make requests and check the expected response. While we can make similar tests from inside the Router's code, these tests here are faster to write and modify because they do not require recompilations of the Router, at the cost of a slightly higher runtime cost. ## How to write a test diff --git a/apollo-router/tests/samples/basic/query1/supergraph.graphql b/apollo-router/tests/samples/basic/query1/supergraph.graphql index 971174056a..c5a920730a 100644 --- a/apollo-router/tests/samples/basic/query1/supergraph.graphql +++ b/apollo-router/tests/samples/basic/query1/supergraph.graphql @@ -1,90 +1,124 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION +scalar join__FieldSet +scalar link__Import -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ +enum link__Purpose { SECURITY + EXECUTION } -scalar join__FieldSet - -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") -} -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + upc: String! } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! author: User @join__field(graph: REVIEWS, provides: "username") body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) product: Product @join__field(graph: REVIEWS) } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/samples/basic/query2/supergraph.graphql b/apollo-router/tests/samples/basic/query2/supergraph.graphql index 971174056a..1bd9f596ee 100644 --- a/apollo-router/tests/samples/basic/query2/supergraph.graphql +++ b/apollo-router/tests/samples/basic/query2/supergraph.graphql @@ -1,90 +1,125 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION - -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION +scalar join__FieldSet - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") } -scalar join__FieldSet +scalar link__Import -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") +enum link__Purpose { + SECURITY + EXECUTION } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + upc: String! } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ - author: User @join__field(graph: REVIEWS, provides: "username") +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) + author: User @join__field(graph: REVIEWS, provides: "username") product: Product @join__field(graph: REVIEWS) } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/supergraph.graphql b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/supergraph.graphql index 630e59c38b..ce3258e776 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/supergraph.graphql +++ b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-entity-key/supergraph.graphql @@ -1,40 +1,59 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/tag/v0.3") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION - -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION - - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY -} +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet +scalar link__Import enum join__Graph { ACCOUNTS @join__graph(name: "invalidation-entity-key-accounts", url: "https://accounts.demo.starstuff.dev") @@ -42,49 +61,61 @@ enum join__Graph { PRODUCTS @join__graph(name: "invalidation-entity-key-products", url: "https://products.demo.starstuff.dev") REVIEWS @join__graph(name: "invalidation-entity-key-reviews", url: "https://reviews.demo.starstuff.dev") } -type Mutation { + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) + @join__type(graph: ACCOUNTS) { updateMyAccount: User @join__field(graph: ACCOUNTS) invalidateProductReview: Int @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + upc: String! } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + body: String + id: ID! + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/supergraph.graphql b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/supergraph.graphql index c8184433b1..e88cb8ecfa 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/supergraph.graphql +++ b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-name/supergraph.graphql @@ -1,40 +1,59 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/tag/v0.3") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION - -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION - - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY -} +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet +scalar link__Import enum join__Graph { ACCOUNTS @join__graph(name: "invalidation-subgraph-name-accounts", url: "https://accounts.demo.starstuff.dev") @@ -42,50 +61,63 @@ enum join__Graph { PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") } -type Mutation { + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) + @join__type(graph: ACCOUNTS) { updateMyAccount: User @join__field(graph: ACCOUNTS) createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + upc: String! } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + body: String + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) - reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + reviews: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/supergraph.graphql b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/supergraph.graphql index a9554a070d..75367a7939 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/supergraph.graphql +++ b/apollo-router/tests/samples/enterprise/entity-cache/invalidation-subgraph-type/supergraph.graphql @@ -1,40 +1,58 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/tag/v0.3") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA - -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION - -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION - - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY -} +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet +scalar link__Import enum join__Graph { ACCOUNTS @join__graph(name: "invalidation-subgraph-type-accounts", url: "https://accounts.demo.starstuff.dev") @@ -42,50 +60,63 @@ enum join__Graph { PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") } -type Mutation { + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) + @join__type(graph: ACCOUNTS) { updateMyAccount: User @join__field(graph: ACCOUNTS) createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + upc: String! } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + body: String + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) - reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + reviews: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/samples/enterprise/entity-cache/private/supergraph.graphql b/apollo-router/tests/samples/enterprise/entity-cache/private/supergraph.graphql index 1196414b6f..1f6a22ae3b 100644 --- a/apollo-router/tests/samples/enterprise/entity-cache/private/supergraph.graphql +++ b/apollo-router/tests/samples/enterprise/entity-cache/private/supergraph.graphql @@ -1,91 +1,129 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/tag/v0.3") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA - -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION +scalar join__FieldSet +scalar link__Import -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ +enum link__Purpose { SECURITY + EXECUTION } -scalar join__FieldSet - -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") -} -type Mutation { +type Mutation + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) + @join__type(graph: ACCOUNTS) { updateMyAccount: User @join__field(graph: ACCOUNTS) createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + upc: String! } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + body: String + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) - reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + reviews: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/samples/enterprise/query-planning-redis/supergraph.graphql b/apollo-router/tests/samples/enterprise/query-planning-redis/supergraph.graphql index 971174056a..9f9fedb1b8 100644 --- a/apollo-router/tests/samples/enterprise/query-planning-redis/supergraph.graphql +++ b/apollo-router/tests/samples/enterprise/query-planning-redis/supergraph.graphql @@ -1,90 +1,129 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1", for: SECURITY) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/tag/v0.3") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA - -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA -directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION +scalar join__FieldSet +scalar link__Import -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ +enum link__Purpose { SECURITY + EXECUTION } -scalar join__FieldSet - -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") -} -type Mutation { +type Mutation + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) + @join__type(graph: ACCOUNTS) { + updateMyAccount: User @join__field(graph: ACCOUNTS) createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - inStock: Boolean @join__field(graph: INVENTORY) @tag(name: "private") @inaccessible + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) + upc: String! } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + body: String + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) - reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + reviews: String @join__field(graph: ACCOUNTS) } diff --git a/apollo-router/tests/set_context.rs b/apollo-router/tests/set_context.rs index bc5f44d2da..47d8deb076 100644 --- a/apollo-router/tests/set_context.rs +++ b/apollo-router/tests/set_context.rs @@ -206,7 +206,10 @@ async fn test_set_context_with_null() { let response = run_single_request( QUERY, &[ - ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ( + "Subgraph1", + include_str!("fixtures/set_context/one_null_param.json"), + ), ("Subgraph2", include_str!("fixtures/set_context/two.json")), ], ) @@ -259,7 +262,10 @@ async fn test_set_context_unrelated_fetch_failure() { let response = run_single_request( QUERY, &[ - ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ( + "Subgraph1", + include_str!("fixtures/set_context/one_fetch_failure.json"), + ), ("Subgraph2", include_str!("fixtures/set_context/two.json")), ], ) @@ -285,7 +291,10 @@ async fn test_set_context_dependent_fetch_failure() { let response = run_single_request( QUERY, &[ - ("Subgraph1", include_str!("fixtures/set_context/one.json")), + ( + "Subgraph1", + include_str!("fixtures/set_context/one_dependent_fetch_failure.json"), + ), ("Subgraph2", include_str!("fixtures/set_context/two.json")), ], ) diff --git a/apollo-router/tests/snapshots/integration_tests__starstuff_supergraph_is_valid.snap b/apollo-router/tests/snapshots/integration_tests__starstuff_supergraph_is_valid.snap index dd48a351f6..71dbc40c36 100644 --- a/apollo-router/tests/snapshots/integration_tests__starstuff_supergraph_is_valid.snap +++ b/apollo-router/tests/snapshots/integration_tests__starstuff_supergraph_is_valid.snap @@ -2,90 +2,124 @@ source: apollo-router/tests/integration_tests.rs expression: "include_str!(\"../../examples/graphql/supergraph.graphql\")" --- - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, provides: join__FieldSet, requires: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on INTERFACE | OBJECT +scalar join__FieldSet -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +scalar link__Import + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) + recommendedProducts: [Product] @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) -} - -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION - - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY } - -scalar join__FieldSet - -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory" url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products" url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews" url: "https://reviews.demo.starstuff.dev") -} - diff --git a/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap index 099d36a7cb..4c44587cdd 100644 --- a/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap +++ b/apollo-router/tests/snapshots/set_context__set_context_dependent_fetch_failure.snap @@ -4,6 +4,21 @@ expression: response --- { "data": null, + "errors": [ + { + "message": "Some error", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": [ + "t", + "u" + ] + } + ], "extensions": { "apolloQueryPlan": { "object": { diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap new file mode 100644 index 0000000000..17f0ec285c --- /dev/null +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled-2.snap @@ -0,0 +1,154 @@ +--- +source: apollo-router/tests/type_conditions.rs +expression: response +--- +{ + "data": { + "search": [ + { + "id": "a7052397-b605-414a-aba4-408d51c8eef0", + "sections": [ + { + "artwork": "Hello World", + "title": "d0182b8a-a671-4244-ba1c-905274b0d198 title" + }, + { + "artwork": "Hello World", + "title": "e6eec2fc-05ce-40a2-956b-f1335e615204 title" + } + ] + }, + { + "id": "3a7b08c9-d8c0-4c55-b55d-596a272392e0", + "sections": [ + { + "artwork": "Hello World", + "title": "f44f584e-5d3d-4466-96f5-9afc3f5d5a54 title" + }, + { + "artwork": "Hello World" + } + ] + }, + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "Hello World" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "Hello World" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + }, + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "Hello World" + }, + { + "artwork": "Hello World", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ] + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "searchSubgraph", + "variableUsages": [], + "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operationName": "Search__searchSubgraph__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "587c887350ef75eaf4b647be94fd682616bcd33909e15fb797cee226e95fa36a", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + }, + { + "kind": "Flatten", + "path": [ + "search", + "@", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "movieResultParam" + ], + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operationName": "Search__artworkSubgraph__1", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "622eb49d4e52ff2636348e103f941d04b783fec97de59d0ae6635d9272f97ad8", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Flatten(path: \"search.@.sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_disabled.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap similarity index 90% rename from apollo-router/tests/snapshots/type_conditions__type_conditions_disabled.snap rename to apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap index 224cd2fb09..f355685192 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_disabled.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_disabled.snap @@ -72,7 +72,7 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0{search{__typename ...on MovieResult{sections{__typename ...on EntityCollectionSection{__typename id}...on GallerySection{__typename id}}id}...on ArticleResult{id sections{__typename ...on GallerySection{__typename id}...on EntityCollectionSection{__typename id}}}}}", + "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, @@ -130,7 +130,7 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){...on EntityCollectionSection{title artwork(params:$movieResultParam)}...on GallerySection{artwork(params:$movieResultParam)}}}", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap new file mode 100644 index 0000000000..01e49a0721 --- /dev/null +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled-2.snap @@ -0,0 +1,218 @@ +--- +source: apollo-router/tests/type_conditions.rs +expression: response +--- +{ + "data": { + "search": [ + { + "id": "a7052397-b605-414a-aba4-408d51c8eef0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "d0182b8a-a671-4244-ba1c-905274b0d198 title" + }, + { + "artwork": "articleResultEnabled artwork", + "title": "e6eec2fc-05ce-40a2-956b-f1335e615204 title" + } + ] + }, + { + "id": "3a7b08c9-d8c0-4c55-b55d-596a272392e0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "f44f584e-5d3d-4466-96f5-9afc3f5d5a54 title" + }, + { + "artwork": "articleResultEnabled artwork" + } + ] + }, + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "movieResultEnabled artwork" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "movieResultEnabled artwork" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + }, + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "movieResultEnabled artwork" + }, + { + "artwork": "movieResultEnabled artwork", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ] + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "searchSubgraph", + "variableUsages": [], + "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operationName": "Search__searchSubgraph__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "587c887350ef75eaf4b647be94fd682616bcd33909e15fb797cee226e95fa36a", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "path": [ + "search", + "@|[ArticleResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "articleResultParam" + ], + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", + "operationName": "Search__artworkSubgraph__1", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "a0bf36d3a611df53c3a60b9b124a2887f2d266858221c606ace0985d101d64bd", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + }, + { + "kind": "Flatten", + "path": [ + "search", + "@|[MovieResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "movieResultParam" + ], + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operationName": "Search__artworkSubgraph__2", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "3e84a53f967bf40d4c08254a94f3fa32a828ab3ad8184a22bb3439c596ecaaf4", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \"search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n Flatten(path: \"search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap similarity index 91% rename from apollo-router/tests/snapshots/type_conditions__type_conditions_enabled.snap rename to apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap index da66cee5c2..a8afd1fa0e 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled.snap @@ -72,7 +72,7 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0{search{__typename ...on MovieResult{sections{__typename ...on EntityCollectionSection{__typename id}...on GallerySection{__typename id}}id}...on ArticleResult{id sections{__typename ...on GallerySection{__typename id}...on EntityCollectionSection{__typename id}}}}}", + "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, @@ -134,7 +134,7 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){...on EntityCollectionSection{title artwork(params:$movieResultParam)}...on GallerySection{artwork(params:$movieResultParam)}}}", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, @@ -194,7 +194,7 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations:[_Any!]!$articleResultParam:String){_entities(representations:$representations){...on GallerySection{artwork(params:$articleResultParam)}...on EntityCollectionSection{artwork(params:$articleResultParam)title}}}", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap new file mode 100644 index 0000000000..ed94ed7a85 --- /dev/null +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments-2.snap @@ -0,0 +1,218 @@ +--- +source: apollo-router/tests/type_conditions.rs +expression: response +--- +{ + "data": { + "search": [ + { + "id": "a7052397-b605-414a-aba4-408d51c8eef0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "d0182b8a-a671-4244-ba1c-905274b0d198 title" + }, + { + "artwork": "articleResultEnabled artwork", + "title": "e6eec2fc-05ce-40a2-956b-f1335e615204 title" + } + ] + }, + { + "id": "3a7b08c9-d8c0-4c55-b55d-596a272392e0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "f44f584e-5d3d-4466-96f5-9afc3f5d5a54 title" + }, + { + "artwork": "articleResultEnabled artwork" + } + ] + }, + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "movieResultEnabled artwork" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "movieResultEnabled artwork" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + }, + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "movieResultEnabled artwork" + }, + { + "artwork": "movieResultEnabled artwork", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ] + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "searchSubgraph", + "variableUsages": [], + "operation": "query Search__searchSubgraph__0 { search { __typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { __typename id } fragment _generated_onGallerySection2_0 on GallerySection { __typename id } fragment _generated_onMovieResult2_0 on MovieResult { sections { __typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0 } id } fragment _generated_onArticleResult2_0 on ArticleResult { id sections { __typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0 } }", + "operationName": "Search__searchSubgraph__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "70ca85b28e861b24a7749862930a5f965c4c6e8074d60a87a3952d596fe7cc36", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "path": [ + "search", + "@|[ArticleResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "articleResultParam" + ], + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", + "operationName": "Search__artworkSubgraph__1", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "1d21a65a3b5a31e17f7834750ef5b37fb49d99d0a1e2145f00a62d43c5f8423a", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + }, + { + "kind": "Flatten", + "path": [ + "search", + "@|[MovieResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "movieResultParam" + ], + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", + "operationName": "Search__artworkSubgraph__2", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "df321f6532c2c9eda0d8c042e5f08073c24e558dd0cae01054886b79416a6c08", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ..._generated_onMovieResult2_0\n ..._generated_onArticleResult2_0\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n __typename\n id\n }\n\n fragment _generated_onGallerySection2_0 on GallerySection {\n __typename\n id\n }\n\n fragment _generated_onMovieResult2_0 on MovieResult {\n sections {\n __typename\n ..._generated_onEntityCollectionSection2_0\n ..._generated_onGallerySection2_0\n }\n id\n }\n\n fragment _generated_onArticleResult2_0 on ArticleResult {\n id\n sections {\n __typename\n ..._generated_onGallerySection2_0\n ..._generated_onEntityCollectionSection2_0\n }\n }\n },\n Parallel {\n Flatten(path: \"search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ..._generated_onEntityCollectionSection2_0\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n },\n },\n Flatten(path: \"search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ..._generated_onEntityCollectionSection2_0\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n\n fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n },\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_generate_query_fragments.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments.snap similarity index 87% rename from apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_generate_query_fragments.snap rename to apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments.snap index e5e2cc616a..08a9782c85 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_generate_query_fragments.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_generate_query_fragments.snap @@ -72,7 +72,7 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0{search{__typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0}}fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection{__typename id}fragment _generated_onGallerySection2_0 on GallerySection{__typename id}fragment _generated_onMovieResult2_0 on MovieResult{sections{__typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0}id}fragment _generated_onArticleResult2_0 on ArticleResult{id sections{__typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0}}", + "operation": "query Search__searchSubgraph__0 { search { __typename ..._generated_onMovieResult2_0 ..._generated_onArticleResult2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { __typename id } fragment _generated_onGallerySection2_0 on GallerySection { __typename id } fragment _generated_onMovieResult2_0 on MovieResult { sections { __typename ..._generated_onEntityCollectionSection2_0 ..._generated_onGallerySection2_0 } id } fragment _generated_onArticleResult2_0 on ArticleResult { id sections { __typename ..._generated_onGallerySection2_0 ..._generated_onEntityCollectionSection2_0 } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, @@ -134,7 +134,7 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){..._generated_onEntityCollectionSection2_0 ...on GallerySection{artwork(params:$movieResultParam)}}}fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection{title artwork(params:$movieResultParam)}", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ..._generated_onEntityCollectionSection2_0 ... on GallerySection { artwork(params: $movieResultParam) } } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { title artwork(params: $movieResultParam) }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, @@ -194,7 +194,7 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations:[_Any!]!$articleResultParam:String){_entities(representations:$representations){...on GallerySection{artwork(params:$articleResultParam)}..._generated_onEntityCollectionSection2_0}}fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection{artwork(params:$articleResultParam)title}", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ..._generated_onEntityCollectionSection2_0 } } fragment _generated_onEntityCollectionSection2_0 on EntityCollectionSection { artwork(params: $articleResultParam) title }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap new file mode 100644 index 0000000000..c990725153 --- /dev/null +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list-2.snap @@ -0,0 +1,282 @@ +--- +source: apollo-router/tests/type_conditions.rs +expression: response +--- +{ + "data": { + "searchListOfList": [ + [ + { + "id": "a7052397-b605-414a-aba4-408d51c8eef0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "d0182b8a-a671-4244-ba1c-905274b0d198 title" + }, + { + "artwork": "articleResultEnabled artwork", + "title": "e6eec2fc-05ce-40a2-956b-f1335e615204 title" + } + ] + }, + { + "id": "3a7b08c9-d8c0-4c55-b55d-596a272392e0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "f44f584e-5d3d-4466-96f5-9afc3f5d5a54 title" + }, + { + "artwork": "articleResultEnabled artwork" + } + ] + }, + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "movieResultEnabled artwork" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "movieResultEnabled artwork" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + }, + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "movieResultEnabled artwork" + }, + { + "artwork": "movieResultEnabled artwork", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ], + [ + { + "id": "a7052397-b605-414a-aba4-408d51c8eef0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "d0182b8a-a671-4244-ba1c-905274b0d198 title" + }, + { + "artwork": "articleResultEnabled artwork", + "title": "e6eec2fc-05ce-40a2-956b-f1335e615204 title" + } + ] + }, + { + "id": "3a7b08c9-d8c0-4c55-b55d-596a272392e0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "f44f584e-5d3d-4466-96f5-9afc3f5d5a54 title" + }, + { + "artwork": "articleResultEnabled artwork" + } + ] + } + ], + [ + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "movieResultEnabled artwork" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "movieResultEnabled artwork" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + } + ], + [ + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "movieResultEnabled artwork" + }, + { + "artwork": "movieResultEnabled artwork", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ] + ] + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "searchSubgraph", + "variableUsages": [], + "operation": "query Search__searchSubgraph__0 { searchListOfList { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operationName": "Search__searchSubgraph__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "365685f6b9f1c5dd02506b27f50d63486f1ca6b5ced7b0253fc050ef73732e03", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "path": [ + "searchListOfList", + "@", + "@|[ArticleResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "articleResultParam" + ], + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", + "operationName": "Search__artworkSubgraph__1", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "a0bf36d3a611df53c3a60b9b124a2887f2d266858221c606ace0985d101d64bd", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + }, + { + "kind": "Flatten", + "path": [ + "searchListOfList", + "@", + "@|[MovieResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "movieResultParam" + ], + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operationName": "Search__artworkSubgraph__2", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "3e84a53f967bf40d4c08254a94f3fa32a828ab3ad8184a22bb3439c596ecaaf4", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfList {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \"searchListOfList.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n Flatten(path: \"searchListOfList.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap similarity index 92% rename from apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list.snap rename to apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap index 9d70336225..adf981893a 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list.snap @@ -134,7 +134,7 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0{searchListOfList{__typename ...on MovieResult{sections{__typename ...on EntityCollectionSection{__typename id}...on GallerySection{__typename id}}id}...on ArticleResult{id sections{__typename ...on GallerySection{__typename id}...on EntityCollectionSection{__typename id}}}}}", + "operation": "query Search__searchSubgraph__0 { searchListOfList { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, @@ -197,7 +197,7 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){...on EntityCollectionSection{title artwork(params:$movieResultParam)}...on GallerySection{artwork(params:$movieResultParam)}}}", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, @@ -258,7 +258,7 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations:[_Any!]!$articleResultParam:String){_entities(representations:$representations){...on GallerySection{artwork(params:$articleResultParam)}...on EntityCollectionSection{artwork(params:$articleResultParam)title}}}", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap new file mode 100644 index 0000000000..f206e94c89 --- /dev/null +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list-2.snap @@ -0,0 +1,288 @@ +--- +source: apollo-router/tests/type_conditions.rs +expression: response +--- +{ + "data": { + "searchListOfListOfList": [ + [ + [ + { + "id": "a7052397-b605-414a-aba4-408d51c8eef0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "d0182b8a-a671-4244-ba1c-905274b0d198 title" + }, + { + "artwork": "articleResultEnabled artwork", + "title": "e6eec2fc-05ce-40a2-956b-f1335e615204 title" + } + ] + }, + { + "id": "3a7b08c9-d8c0-4c55-b55d-596a272392e0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "f44f584e-5d3d-4466-96f5-9afc3f5d5a54 title" + }, + { + "artwork": "articleResultEnabled artwork" + } + ] + }, + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "movieResultEnabled artwork" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "movieResultEnabled artwork" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + }, + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "movieResultEnabled artwork" + }, + { + "artwork": "movieResultEnabled artwork", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ], + [ + { + "id": "a7052397-b605-414a-aba4-408d51c8eef0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "d0182b8a-a671-4244-ba1c-905274b0d198 title" + }, + { + "artwork": "articleResultEnabled artwork", + "title": "e6eec2fc-05ce-40a2-956b-f1335e615204 title" + } + ] + }, + { + "id": "3a7b08c9-d8c0-4c55-b55d-596a272392e0", + "sections": [ + { + "artwork": "articleResultEnabled artwork", + "title": "f44f584e-5d3d-4466-96f5-9afc3f5d5a54 title" + }, + { + "artwork": "articleResultEnabled artwork" + } + ] + } + ] + ], + [ + [ + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "movieResultEnabled artwork" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "movieResultEnabled artwork" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + } + ], + [ + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "movieResultEnabled artwork" + }, + { + "artwork": "movieResultEnabled artwork", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ] + ] + ] + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "searchSubgraph", + "variableUsages": [], + "operation": "query Search__searchSubgraph__0 { searchListOfListOfList { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operationName": "Search__searchSubgraph__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "47d7643dd7226529814a57e0382aafb3790c2d8a8b26354aa2f60e9c9f097a05", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "path": [ + "searchListOfListOfList", + "@", + "@", + "@|[ArticleResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "articleResultParam" + ], + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", + "operationName": "Search__artworkSubgraph__1", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "a0bf36d3a611df53c3a60b9b124a2887f2d266858221c606ace0985d101d64bd", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + }, + { + "kind": "Flatten", + "path": [ + "searchListOfListOfList", + "@", + "@", + "@|[MovieResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "movieResultParam" + ], + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operationName": "Search__artworkSubgraph__2", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "3e84a53f967bf40d4c08254a94f3fa32a828ab3ad8184a22bb3439c596ecaaf4", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n searchListOfListOfList {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \"searchListOfListOfList.@.@.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n Flatten(path: \"searchListOfListOfList.@.@.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list_of_list.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap similarity index 92% rename from apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list_of_list.snap rename to apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap index 5a6a4b30bc..78a539ec02 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_list_of_list_of_list.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_list_of_list_of_list.snap @@ -138,7 +138,7 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0{searchListOfListOfList{__typename ...on MovieResult{sections{__typename ...on EntityCollectionSection{__typename id}...on GallerySection{__typename id}}id}...on ArticleResult{id sections{__typename ...on GallerySection{__typename id}...on EntityCollectionSection{__typename id}}}}}", + "operation": "query Search__searchSubgraph__0 { searchListOfListOfList { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, @@ -202,7 +202,7 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){...on EntityCollectionSection{title artwork(params:$movieResultParam)}...on GallerySection{artwork(params:$movieResultParam)}}}", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, @@ -264,7 +264,7 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations:[_Any!]!$articleResultParam:String){_entities(representations:$representations){...on GallerySection{artwork(params:$articleResultParam)}...on EntityCollectionSection{artwork(params:$articleResultParam)title}}}", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, diff --git a/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap new file mode 100644 index 0000000000..41a6433f9f --- /dev/null +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch-2.snap @@ -0,0 +1,193 @@ +--- +source: apollo-router/tests/type_conditions.rs +expression: response +--- +{ + "data": { + "search": [ + { + "sections": [ + { + "id": "d9077ad2-d79a-45b5-b5ee-25ded226f03c", + "title": "d9077ad2-d79a-45b5-b5ee-25ded226f03c title", + "artwork": "movieResultEnabled artwork" + }, + { + "id": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02", + "title": "9f1f1ebb-21d3-4afe-bb7d-6de706f78f02 title", + "artwork": "movieResultEnabled artwork" + } + ], + "id": "c5f4985f-8fb6-4414-a3f5-56f7f58dd043" + }, + { + "sections": [ + { + "id": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12", + "title": "24cea0de-2ac8-4cbe-85b6-8b1b80647c12 title", + "artwork": "movieResultEnabled artwork" + }, + { + "artwork": "movieResultEnabled artwork", + "id": "2f772201-42ca-4376-9871-2252cc052262" + } + ], + "id": "ff140d35-ce5d-48fe-bad7-1cfb2c3e310a" + } + ] + }, + "extensions": { + "apolloQueryPlan": { + "object": { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "searchSubgraph", + "variableUsages": [], + "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", + "operationName": "Search__searchSubgraph__0", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "587c887350ef75eaf4b647be94fd682616bcd33909e15fb797cee226e95fa36a", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "path": [ + "search", + "@|[ArticleResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "articleResultParam" + ], + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", + "operationName": "Search__artworkSubgraph__1", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "a0bf36d3a611df53c3a60b9b124a2887f2d266858221c606ace0985d101d64bd", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + }, + { + "kind": "Flatten", + "path": [ + "search", + "@|[MovieResult]", + "sections", + "@" + ], + "node": { + "kind": "Fetch", + "serviceName": "artworkSubgraph", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "EntityCollectionSection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "GallerySection", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [ + "movieResultParam" + ], + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", + "operationName": "Search__artworkSubgraph__2", + "operationKind": "query", + "id": null, + "inputRewrites": null, + "outputRewrites": null, + "contextRewrites": null, + "schemaAwareHash": "3e84a53f967bf40d4c08254a94f3fa32a828ab3ad8184a22bb3439c596ecaaf4", + "authorization": { + "is_authenticated": false, + "scopes": [], + "policies": [] + } + } + } + ] + } + ] + } + }, + "text": "QueryPlan {\n Sequence {\n Fetch(service: \"searchSubgraph\") {\n {\n search {\n __typename\n ... on MovieResult {\n sections {\n __typename\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n }\n id\n }\n ... on ArticleResult {\n id\n sections {\n __typename\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n }\n }\n }\n }\n },\n Parallel {\n Flatten(path: \"search.@|[ArticleResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on GallerySection {\n __typename\n id\n }\n ... on EntityCollectionSection {\n __typename\n id\n }\n } =>\n {\n ... on GallerySection {\n artwork(params: $articleResultParam)\n }\n ... on EntityCollectionSection {\n artwork(params: $articleResultParam)\n title\n }\n }\n },\n },\n Flatten(path: \"search.@|[MovieResult].sections.@\") {\n Fetch(service: \"artworkSubgraph\") {\n {\n ... on EntityCollectionSection {\n __typename\n id\n }\n ... on GallerySection {\n __typename\n id\n }\n } =>\n {\n ... on EntityCollectionSection {\n title\n artwork(params: $movieResultParam)\n }\n ... on GallerySection {\n artwork(params: $movieResultParam)\n }\n }\n },\n },\n },\n },\n}" + } + } +} diff --git a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_shouldnt_make_article_fetch.snap b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch.snap similarity index 90% rename from apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_shouldnt_make_article_fetch.snap rename to apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch.snap index acd8fb6676..c8fe1fb487 100644 --- a/apollo-router/tests/snapshots/type_conditions__type_conditions_enabled_shouldnt_make_article_fetch.snap +++ b/apollo-router/tests/snapshots/type_conditions___test_type_conditions_enabled_shouldnt_make_article_fetch.snap @@ -47,7 +47,7 @@ expression: response "kind": "Fetch", "serviceName": "searchSubgraph", "variableUsages": [], - "operation": "query Search__searchSubgraph__0{search{__typename ...on MovieResult{sections{__typename ...on EntityCollectionSection{__typename id}...on GallerySection{__typename id}}id}...on ArticleResult{id sections{__typename ...on GallerySection{__typename id}...on EntityCollectionSection{__typename id}}}}}", + "operation": "query Search__searchSubgraph__0 { search { __typename ... on MovieResult { sections { __typename ... on EntityCollectionSection { __typename id } ... on GallerySection { __typename id } } id } ... on ArticleResult { id sections { __typename ... on GallerySection { __typename id } ... on EntityCollectionSection { __typename id } } } } }", "operationName": "Search__searchSubgraph__0", "operationKind": "query", "id": null, @@ -109,7 +109,7 @@ expression: response "variableUsages": [ "movieResultParam" ], - "operation": "query Search__artworkSubgraph__1($representations:[_Any!]!$movieResultParam:String){_entities(representations:$representations){...on EntityCollectionSection{title artwork(params:$movieResultParam)}...on GallerySection{artwork(params:$movieResultParam)}}}", + "operation": "query Search__artworkSubgraph__1($representations: [_Any!]!, $movieResultParam: String) { _entities(representations: $representations) { ... on EntityCollectionSection { title artwork(params: $movieResultParam) } ... on GallerySection { artwork(params: $movieResultParam) } } }", "operationName": "Search__artworkSubgraph__1", "operationKind": "query", "id": null, @@ -169,7 +169,7 @@ expression: response "variableUsages": [ "articleResultParam" ], - "operation": "query Search__artworkSubgraph__2($representations:[_Any!]!$articleResultParam:String){_entities(representations:$representations){...on GallerySection{artwork(params:$articleResultParam)}...on EntityCollectionSection{artwork(params:$articleResultParam)title}}}", + "operation": "query Search__artworkSubgraph__2($representations: [_Any!]!, $articleResultParam: String) { _entities(representations: $representations) { ... on GallerySection { artwork(params: $articleResultParam) } ... on EntityCollectionSection { artwork(params: $articleResultParam) title } } }", "operationName": "Search__artworkSubgraph__2", "operationKind": "query", "id": null, diff --git a/apollo-router/tests/type_conditions.rs b/apollo-router/tests/type_conditions.rs index aa109807d2..88757228b8 100644 --- a/apollo-router/tests/type_conditions.rs +++ b/apollo-router/tests/type_conditions.rs @@ -2,6 +2,7 @@ //! Please ensure that any tests added to this file use the tokio multi-threaded test executor. //! +use apollo_compiler::ast::Document; use apollo_router::graphql::Request; use apollo_router::graphql::Response; use apollo_router::plugin::test::MockSubgraph; @@ -29,9 +30,45 @@ struct RequestAndResponse { #[tokio::test(flavor = "multi_thread")] async fn test_type_conditions_enabled() { + _test_type_conditions_enabled("legacy").await; + _test_type_conditions_enabled("new").await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_type_conditions_enabled_generate_query_fragments() { + _test_type_conditions_enabled_generate_query_fragments("legacy").await; + _test_type_conditions_enabled_generate_query_fragments("new").await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_type_conditions_enabled_list_of_list() { + _test_type_conditions_enabled_list_of_list("legacy").await; + _test_type_conditions_enabled_list_of_list("new").await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_type_conditions_enabled_list_of_list_of_list() { + _test_type_conditions_enabled_list_of_list_of_list("legacy").await; + _test_type_conditions_enabled_list_of_list_of_list("new").await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_type_conditions_disabled() { + _test_type_conditions_disabled("legacy").await; + _test_type_conditions_disabled("new").await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_type_conditions_enabled_shouldnt_make_article_fetch() { + _test_type_conditions_enabled_shouldnt_make_article_fetch("legacy").await; + _test_type_conditions_enabled_shouldnt_make_article_fetch("new").await; +} + +async fn _test_type_conditions_enabled(planner_mode: &str) -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": true, + "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -70,14 +107,17 @@ async fn test_type_conditions_enabled() { .await .unwrap(); + let response = normalize_response_extensions(response); + insta::assert_json_snapshot!(response); + response } -#[tokio::test(flavor = "multi_thread")] -async fn test_type_conditions_enabled_generate_query_fragments() { +async fn _test_type_conditions_enabled_generate_query_fragments(planner_mode: &str) -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": true, + "experimental_query_planner_mode": planner_mode, "supergraph": { "generate_query_fragments": true }, @@ -119,14 +159,17 @@ async fn test_type_conditions_enabled_generate_query_fragments() { .await .unwrap(); + let response = normalize_response_extensions(response); + insta::assert_json_snapshot!(response); + response } -#[tokio::test(flavor = "multi_thread")] -async fn test_type_conditions_enabled_list_of_list() { +async fn _test_type_conditions_enabled_list_of_list(planner_mode: &str) -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": true, + "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -165,15 +208,18 @@ async fn test_type_conditions_enabled_list_of_list() { .await .unwrap(); + let response = normalize_response_extensions(response); + insta::assert_json_snapshot!(response); + response } // one last to make sure unnesting is correct -#[tokio::test(flavor = "multi_thread")] -async fn test_type_conditions_enabled_list_of_list_of_list() { +async fn _test_type_conditions_enabled_list_of_list_of_list(planner_mode: &str) -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": true, + "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -212,14 +258,17 @@ async fn test_type_conditions_enabled_list_of_list_of_list() { .await .unwrap(); + let response = normalize_response_extensions(response); + insta::assert_json_snapshot!(response); + response } -#[tokio::test(flavor = "multi_thread")] -async fn test_type_conditions_disabled() { +async fn _test_type_conditions_disabled(planner_mode: &str) -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": false, + "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -257,14 +306,17 @@ async fn test_type_conditions_disabled() { .await .unwrap(); + let response = normalize_response_extensions(response); + insta::assert_json_snapshot!(response); + response } -#[tokio::test(flavor = "multi_thread")] -async fn test_type_conditions_enabled_shouldnt_make_article_fetch() { +async fn _test_type_conditions_enabled_shouldnt_make_article_fetch(planner_mode: &str) -> Response { let harness = setup_from_mocks( json! {{ "experimental_type_conditioned_fetching": true, + "experimental_query_planner_mode": planner_mode, // will make debugging easier "plugins": { "experimental.expose_query_plan": true @@ -303,7 +355,10 @@ async fn test_type_conditions_enabled_shouldnt_make_article_fetch() { .await .unwrap(); + let response = normalize_response_extensions(response); + insta::assert_json_snapshot!(response); + response } fn setup_from_mocks( @@ -431,3 +486,45 @@ query Search($movieResultParam: String, $articleResultParam: String) { } } }"#; + +fn normalize_response_extensions(mut response: Response) -> Response { + let extensions = &mut response.extensions; + + for (key, value) in extensions.iter_mut() { + visit_object(key, value, &mut |key, value| { + if key.as_str() == "operation" { + if let Value::String(s) = value { + let new_value = Document::parse(s.as_str(), key.as_str()) + .unwrap() + .serialize() + .no_indent() + .to_string(); + *value = Value::String(new_value.into()); + } + } + }); + } + response +} + +fn visit_object(key: &ByteString, value: &mut Value, cb: &mut impl FnMut(&ByteString, &mut Value)) { + cb(key, value); + + match value { + Value::Object(o) => { + for (key, value) in o.iter_mut() { + visit_object(key, value, cb); + } + } + Value::Array(a) => { + for v in a.iter_mut() { + if let Some(m) = v.as_object_mut() { + for (k, v) in m.iter_mut() { + visit_object(k, v, cb); + } + } + } + } + _ => {} + } +} diff --git a/dockerfiles/tracing/docker-compose.datadog.yml b/dockerfiles/tracing/docker-compose.datadog.yml index fc25cbba4f..3b58c1d702 100644 --- a/dockerfiles/tracing/docker-compose.datadog.yml +++ b/dockerfiles/tracing/docker-compose.datadog.yml @@ -3,7 +3,7 @@ services: apollo-router: container_name: apollo-router - image: ghcr.io/apollographql/router:v1.54.0 + image: ghcr.io/apollographql/router:v1.55.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/datadog.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.jaeger.yml b/dockerfiles/tracing/docker-compose.jaeger.yml index fd27332b78..fcb75930d2 100644 --- a/dockerfiles/tracing/docker-compose.jaeger.yml +++ b/dockerfiles/tracing/docker-compose.jaeger.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router #build: ./router - image: ghcr.io/apollographql/router:v1.54.0 + image: ghcr.io/apollographql/router:v1.55.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/jaeger.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml index 4129159c1b..0cb933a7ac 100644 --- a/dockerfiles/tracing/docker-compose.zipkin.yml +++ b/dockerfiles/tracing/docker-compose.zipkin.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router build: ./router - image: ghcr.io/apollographql/router:v1.54.0 + image: ghcr.io/apollographql/router:v1.55.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/zipkin.router.yaml:/etc/config/configuration.yaml diff --git a/docs/source/configuration/distributed-caching.mdx b/docs/source/configuration/distributed-caching.mdx index e1c365e0a3..fca1123ae1 100644 --- a/docs/source/configuration/distributed-caching.mdx +++ b/docs/source/configuration/distributed-caching.mdx @@ -143,6 +143,7 @@ supergraph: #tls: required_to_start: false # Optional, defaults to false reset_ttl: true # Optional, defaults to true + pool_size: 4 # Optional, defaults to 1 ``` #### Timeout @@ -168,4 +169,8 @@ When active, the `required_to_start` option will prevent the router from startin ### Reset TTL -When this option is active, accessing a cache entry in Redis will reset its expiration. \ No newline at end of file +When this option is active, accessing a cache entry in Redis will reset its expiration. + +### Pool size + +The `pool_size` option defines the number of connections to Redis that the router will open. By default, the router will open a single connection to Redis. If there is a lot of traffic between router and Redis and/or there is some latency in thos requests, it is recommended to increase the pool size to reduce that latency. \ No newline at end of file diff --git a/docs/source/configuration/entity-caching.mdx b/docs/source/configuration/entity-caching.mdx index a1f1c2610e..7cff65e71a 100644 --- a/docs/source/configuration/entity-caching.mdx +++ b/docs/source/configuration/entity-caching.mdx @@ -211,7 +211,7 @@ telemetry: cache: # Cache instruments configuration apollo.router.operations.entity.cache: # A counter which counts the number of cache hit and miss for subgraph requests attributes: - entity.type: true # Include the entity type name. default: false + graphql.type.name: true # Include the entity type name. default: false subgraph.name: # Custom attributes to include the subgraph name in the metric subgraph_name: true supergraph.operation.name: # Add custom attribute to display the supergraph operation name diff --git a/docs/source/configuration/in-memory-caching.mdx b/docs/source/configuration/in-memory-caching.mdx index 000fade2e5..e90f6c1d3a 100644 --- a/docs/source/configuration/in-memory-caching.mdx +++ b/docs/source/configuration/in-memory-caching.mdx @@ -48,9 +48,7 @@ supergraph: When loading a new schema, a query plan might change for some queries, so cached query plans cannot be reused. -To prevent increased latency upon query plan cache invalidation, the router precomputes query plans for: -* The most used queries from the cache. -* The entire list of persisted queries. +To prevent increased latency upon query plan cache invalidation, the router precomputes query plans for the most used queries from the cache when a new schema is loaded. Precomputed plans will be cached before the router switches traffic over to the new schema. @@ -63,6 +61,8 @@ supergraph: warmed_up_queries: 100 ``` +(In addition, the router can use the contents of the [persisted query list](./persisted-queries) to prewarm the cache. By default, it does this when loading a new schema but not on startup; you can [configure](./persisted-queries#persisted-queries#experimental_prewarm_query_plan_cache) it to change either of these defaults.) + To get more information on the planning and warm-up process use the following metrics (where `` can be `redis` for distributed cache or `memory`): * counters: diff --git a/docs/source/configuration/persisted-queries.mdx b/docs/source/configuration/persisted-queries.mdx index bb2ba65d5e..0caabc7d1d 100644 --- a/docs/source/configuration/persisted-queries.mdx +++ b/docs/source/configuration/persisted-queries.mdx @@ -76,12 +76,14 @@ If used with the [`safelist`](#safelist) option, the router logs unregistered an -Adding `experimental_prewarm_query_plan_cache: true` to `persisted_queries` configures the router to prewarm the query plan cache on startup using the persisted query list. All subsequent requests benefit from the warmed cache, reducing latency and enhancing performance. +By default, the router [prewarms the query plan cache](./in-memory-caching#cache-warm-up) using all operations on the PQL when a new schema is loaded, but not at startup. Using the `experimental_prewarm_query_plan_cache` option, you can tell the router to prewarm the cache using the PQL on startup as well, or tell it not to prewarm the cache when reloading the schema. (This does not affect whether the router prewarms the query plan cache with recently-used operations from its in-memory cache.) Prewarming the cache means can reduce request latency by ensuring that operations are pre-planned when requests are received, but can make startup or schema reloads slower. ```yaml title="router.yaml" persisted_queries: enabled: true - experimental_prewarm_query_plan_cache: true # default: false + experimental_prewarm_query_plan_cache: + on_startup: true # default: false + on_reload: false # default: true ``` #### `experimental_local_manifests` diff --git a/docs/source/configuration/telemetry/instrumentation/standard-attributes.mdx b/docs/source/configuration/telemetry/instrumentation/standard-attributes.mdx index 4d71bedc64..5df84f52bf 100644 --- a/docs/source/configuration/telemetry/instrumentation/standard-attributes.mdx +++ b/docs/source/configuration/telemetry/instrumentation/standard-attributes.mdx @@ -22,6 +22,21 @@ telemetry: http.response.status_code: true ``` +### Alias + +If you don't want to use the standard name you can still create an alias and use that alias for the name, for example: + +```yaml title="router.yaml" +telemetry: + instrumentation: + spans: + router: + attributes: + # Standard attributes + http.response.status_code: + alias: status_code # It will be named status_code instead of http.response.status_code +``` + ### Attribute configuration reference diff --git a/docs/source/customizations/rhai-api.mdx b/docs/source/customizations/rhai-api.mdx index a132f94be5..b75824048d 100644 --- a/docs/source/customizations/rhai-api.mdx +++ b/docs/source/customizations/rhai-api.mdx @@ -362,6 +362,10 @@ Router.APOLLO_AUTHENTICATION_JWT_CLAIMS // Context key to access authentication Router.APOLLO_SUBSCRIPTION_WS_CUSTOM_CONNECTION_PARAMS // Context key to modify or access the custom connection params when using subscriptions in WebSocket to subgraphs (cf subscription docs) Router.APOLLO_ENTITY_CACHE_KEY // Context key to access the entity cache key Router.APOLLO_OPERATION_ID // Context key to get the value of apollo operation id (studio trace id) from the context +Router.APOLLO_COST_ESTIMATED_KEY // Context key to get the estimated cost of an operation +Router.APOLLO_COST_ACTUAL_KEY // Context key to get the actual cost of an operation +Router.APOLLO_COST_STRATEGY_KEY // Context key to get the strategy used to calculate cost +Router.APOLLO_COST_RESULT_KEY // Context key to get the cost result of an operation ``` ## `Request` interface diff --git a/docs/source/federation-version-support.mdx b/docs/source/federation-version-support.mdx index 331b9eb7d2..80ff912745 100644 --- a/docs/source/federation-version-support.mdx +++ b/docs/source/federation-version-support.mdx @@ -37,7 +37,15 @@ The table below shows which version of federation each router release is compile - v1.53.0 and later (see latest releases) + v1.55.0 and later (see latest releases) + + + 2.9.1 + + + + + v1.52.1 - v1.53.0 2.9.0 diff --git a/docs/source/quickstart.mdx b/docs/source/quickstart.mdx index f46cb761ed..9e6649d1a7 100644 --- a/docs/source/quickstart.mdx +++ b/docs/source/quickstart.mdx @@ -4,12 +4,12 @@ subtitle: Run the router with GraphOS and Apollo-hosted subgraphs description: This quickstart tutorial walks you through installing the Apollo GraphOS Router or Apollo Router Core and running it with GraphOS and some example Apollo-hosted subgraphs. --- -import ElasticNotice from '../shared/elastic-notice.mdx'; +import ElasticNotice from "../shared/elastic-notice.mdx"; Hello! This tutorial walks you through installing the router (GraphOS Router or Apollo Router Core) and running it in with GraphOS and some example Apollo-hosted subgraphs. -> **This quickstart helps you run a _self-hosted_ instance of the router.** If you [create a cloud supergraph](/graphos/quickstart/cloud/) with Apollo GraphOS, Apollo provisions and hosts your supergraph's GraphOS -Router for you. +> **This quickstart helps you run a _self-hosted_ instance of the router.** If you [create a cloud supergraph](/graphos/quickstart/cloud/) with Apollo GraphOS, Apollo provisions and hosts your supergraph's GraphOS +> Router for you. > > Cloud supergraphs are recommended for organizations that don't need to host their router in their own infrastructure. @@ -17,10 +17,8 @@ Router for you. - ### Download options - #### Automatic download (Linux, OSX, WSL) If you have a bash-compatible terminal, you can download the latest version of the Apollo Router Core directly to your current directory with the following command: @@ -33,12 +31,12 @@ curl -sSL https://router.apollo.dev/download/nix/latest | sh Go to the Apollo Router Core's [GitHub Releases page](https://github.com/apollographql/router/releases) and download the latest `.tar.gz` file that matches your system. Currently, tarballs are available for the following: -* Linux (x86_64) -* Linux (aarch64) -* macOS (Apple Silicon) -* Windows (x86_64) +- Linux (x86_64) +- Linux (aarch64) +- macOS (Apple Silicon) +- Windows (x86_64) -If a tarball for your system or architecture isn't available, you can [build and run the router from source](https://github.com/apollographql/router/blob/HEAD/DEVELOPMENT.md#development-1). You can also [open an issue on GitHub](https://github.com/apollographql/router/issues/new/choose) to request the addition of new architectures. +If a tarball for your system or architecture isn't available, you can [build and run the router from source](https://github.com/apollographql/router/blob/HEAD/DEVELOPMENT.md#development-1). You can also [open an issue on GitHub](https://github.com/apollographql/router/issues/new/choose) to request the addition of new architectures. After downloading, extract the file by running the following from a new project directory, substituting the path to the tarball: @@ -106,77 +104,132 @@ This saves a `supergraph-schema.graphql` file with the following contents: ```graphql title="supergraph-schema.graphql" schema - @core(feature: "https://specs.apollo.dev/core/v0.1"), - @core(feature: "https://specs.apollo.dev/join/v0.1") -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA - -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + scalar join__FieldSet enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory" url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products" url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews" url: "https://reviews.demo.starstuff.dev") + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) + recommendedProducts: [Product] @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } ``` @@ -216,6 +269,6 @@ Visit `http://127.0.0.1:4000` to open Apollo Sandbox, inspect your entire superg Now that you know how to run the router with a supergraph schema, you can: -* Set up [managed federation](/federation/managed-federation/overview) -* Learn about [additional configuration options](./configuration/overview) -* [Estimate the system resources needed to deploy the router](/technotes/TN0045-router_resource_estimator/). +- Set up [managed federation](/federation/managed-federation/overview) +- Learn about [additional configuration options](./configuration/overview) +- [Estimate the system resources needed to deploy the router](/technotes/TN0045-router_resource_estimator/). diff --git a/examples/graphql/federation2.graphql b/examples/graphql/federation2.graphql deleted file mode 100644 index 7ec10a34d8..0000000000 --- a/examples/graphql/federation2.graphql +++ /dev/null @@ -1,134 +0,0 @@ - -schema - @core(feature: "https://specs.apollo.dev/core/v0.2") - @core(feature: "https://specs.apollo.dev/join/v0.2", for: EXECUTION) -{ - query: Query -} - -directive @core(feature: String!, as: String, for: core__Purpose) repeatable on SCHEMA - -directive @join__field(graph: join__Graph!, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION - -directive @join__graph(name: String!, url: String!) on ENUM_VALUE - -directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE - -directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR - -enum core__Purpose { - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY - - """ - `EXECUTION` features provide metadata necessary for operation execution. - """ - EXECUTION -} - -type DeliveryEstimates - @join__type(graph: INVENTORY) -{ - estimatedDelivery: String - fastestDelivery: String -} - -scalar join__FieldSet - -enum join__Graph { - USERS @join__graph(name: "users", url: "http://localhost:4001") - PANDAS @join__graph(name: "pandas", url: "http://localhost:4002") - PRODUCTS @join__graph(name: "products", url: "http://localhost:4003") - INVENTORY @join__graph(name: "inventory", url: "http://localhost:4004/graphql") -} - -type Panda - @join__type(graph: PANDAS) -{ - name: ID! - favoriteFood: String -} - -type Product implements ProductItf & SkuItf - @join__implements(graph: INVENTORY, interface: "ProductItf") - @join__implements(graph: PRODUCTS, interface: "ProductItf") - @join__implements(graph: PRODUCTS, interface: "SkuItf") - @join__type(graph: INVENTORY, key: "id") - @join__type(graph: PRODUCTS, key: "id") - @join__type(graph: PRODUCTS, key: "sku package") - @join__type(graph: PRODUCTS, key: "sku variation { id }") -{ - id: ID! - dimensions: ProductDimension @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) - delivery(zip: String): DeliveryEstimates @join__field(graph: INVENTORY, requires: "dimensions { size weight }") - sku: String @join__field(graph: PRODUCTS) - package: String @join__field(graph: PRODUCTS) - variation: ProductVariation @join__field(graph: PRODUCTS) - createdBy: User @join__field(graph: PRODUCTS) -} - -type ProductDimension - @join__type(graph: INVENTORY) - @join__type(graph: PRODUCTS) -{ - size: String - weight: Float -} - -interface ProductItf implements SkuItf - @join__implements(graph: PRODUCTS, interface: "SkuItf") - @join__type(graph: INVENTORY) - @join__type(graph: PRODUCTS) -{ - id: ID! - dimensions: ProductDimension - delivery(zip: String): DeliveryEstimates @join__field(graph: INVENTORY) - sku: String @join__field(graph: PRODUCTS) - package: String @join__field(graph: PRODUCTS) - variation: ProductVariation @join__field(graph: PRODUCTS) - createdBy: User @join__field(graph: PRODUCTS) -} - -type ProductVariation - @join__type(graph: PRODUCTS) -{ - id: ID! -} - -type Query - @join__type(graph: INVENTORY) - @join__type(graph: PANDAS) - @join__type(graph: PRODUCTS) - @join__type(graph: USERS) -{ - allPandas: [Panda] @join__field(graph: PANDAS) - panda(name: ID!): Panda @join__field(graph: PANDAS) - allProducts: [ProductItf] @join__field(graph: PRODUCTS) - product(id: ID!): ProductItf @join__field(graph: PRODUCTS) -} - -enum ShippingClass - @join__type(graph: INVENTORY) - @join__type(graph: PRODUCTS) -{ - STANDARD - EXPRESS - OVERNIGHT -} - -interface SkuItf - @join__type(graph: PRODUCTS) -{ - sku: String -} - -type User - @join__type(graph: PRODUCTS, key: "email") - @join__type(graph: USERS, key: "email") -{ - email: ID! - totalProductsCreated: Int - name: String @join__field(graph: USERS) -} \ No newline at end of file diff --git a/examples/graphql/local.graphql b/examples/graphql/local.graphql index 3090cbec59..5e39bb3d85 100644 --- a/examples/graphql/local.graphql +++ b/examples/graphql/local.graphql @@ -1,84 +1,120 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") { + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query - mutation: Mutation subscription: Subscription + mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE directive @join__field( graph: join__Graph requires: join__FieldSet provides: join__FieldSet -) on FIELD_DEFINITION + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE directive @join__type( graph: join__Graph! key: join__FieldSet -) repeatable on OBJECT | INTERFACE + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA scalar join__FieldSet +scalar link__Import enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001") - INVENTORY @join__graph(name: "inventory", url: "http://localhost:4004") - PRODUCTS @join__graph(name: "products", url: "http://localhost:4003") - REVIEWS @join__graph(name: "reviews", url: "http://localhost:4002") + ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:4001/graphql") + INVENTORY + @join__graph(name: "inventory", url: "http://localhost:4002/graphql") + PRODUCTS @join__graph(name: "products", url: "http://localhost:4003/graphql") + REVIEWS @join__graph(name: "reviews", url: "http://localhost:4004/graphql") } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review - @join__field(graph: REVIEWS) +enum link__Purpose { + SECURITY + EXECUTION } -type Subscription { +type Subscription @join__type(graph: ACCOUNTS) { userWasCreated(intervalMs: Int, nbEvents: Int): User @join__field(graph: ACCOUNTS) } +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) +} + type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") { +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! author: User @join__field(graph: REVIEWS, provides: "username") body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) product: Product @join__field(graph: REVIEWS) } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") @join__type(graph: REVIEWS, key: "id") { - id: ID! @join__field(graph: ACCOUNTS) + id: ID! name: String @join__field(graph: ACCOUNTS) createdAt: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) } diff --git a/examples/graphql/supergraph-fed1.graphql b/examples/graphql/supergraph-fed1.graphql new file mode 100644 index 0000000000..dadcc318f0 --- /dev/null +++ b/examples/graphql/supergraph-fed1.graphql @@ -0,0 +1,90 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.2") + @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) { + query: Query + mutation: Mutation +} + +directive @core( + as: String + feature: String! + for: core__Purpose +) repeatable on SCHEMA + +directive @join__field( + graph: join__Graph + provides: join__FieldSet + requires: join__FieldSet +) on FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT + +directive @join__type( + graph: join__Graph! + key: join__FieldSet +) repeatable on INTERFACE | OBJECT + +type Mutation { + createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) +} + +type Product + @join__owner(graph: PRODUCTS) + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean @join__field(graph: INVENTORY) + name: String @join__field(graph: PRODUCTS) + price: Int @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) + reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + upc: String! @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: PRODUCTS) +} + +type Query { + me: User @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) +} + +type Review + @join__owner(graph: REVIEWS) + @join__type(graph: REVIEWS, key: "id") { + author: User @join__field(graph: REVIEWS, provides: "username") + body: String @join__field(graph: REVIEWS) + id: ID! @join__field(graph: REVIEWS) + product: Product @join__field(graph: REVIEWS) +} + +type User + @join__owner(graph: ACCOUNTS) + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! @join__field(graph: ACCOUNTS) + name: String @join__field(graph: ACCOUNTS) + reviews: [Review] @join__field(graph: REVIEWS) + username: String @join__field(graph: ACCOUNTS) +} + +enum core__Purpose { + EXECUTION + SECURITY +} + +scalar join__FieldSet + +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev") + INVENTORY + @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev") +} diff --git a/examples/graphql/supergraph-fed2.graphql b/examples/graphql/supergraph-fed2.graphql deleted file mode 100644 index 504fbbaafb..0000000000 --- a/examples/graphql/supergraph-fed2.graphql +++ /dev/null @@ -1,98 +0,0 @@ -schema - @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) -{ - query: Query - mutation: Mutation -} - -directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE - -directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION - -directive @join__graph(name: String!, url: String!) on ENUM_VALUE - -directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE - -directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR - -directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION - -directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA - -scalar join__FieldSet - -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") - INVENTORY @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev/") - PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") - REVIEWS @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") -} - -scalar link__Import - -enum link__Purpose { - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY - - """ - `EXECUTION` features provide metadata necessary for operation execution. - """ - EXECUTION -} - -type Mutation - @join__type(graph: PRODUCTS) - @join__type(graph: REVIEWS) -{ - createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) - createReview(upc: ID!, id: ID!, body: String): Review @join__field(graph: REVIEWS) -} - -type Product - @join__type(graph: ACCOUNTS, key: "upc", extension: true) - @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: PRODUCTS, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ - upc: String! - weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) - price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) - inStock: Boolean @join__field(graph: INVENTORY) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - name: String @join__field(graph: PRODUCTS) - reviews: [Review] @join__field(graph: REVIEWS) - reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) -} - -type Query - @join__type(graph: ACCOUNTS) - @join__type(graph: INVENTORY) - @join__type(graph: PRODUCTS) - @join__type(graph: REVIEWS) -{ - me: User @join__field(graph: ACCOUNTS) - recommendedProducts: [Product] @join__field(graph: ACCOUNTS) - topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) -} - -type Review - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! - body: String - author: User @join__field(graph: REVIEWS, provides: "username") - product: Product -} - -type User - @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! - name: String @join__field(graph: ACCOUNTS) - username: String @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS, external: true) - reviews: [Review] @join__field(graph: REVIEWS) -} diff --git a/examples/graphql/supergraph.graphql b/examples/graphql/supergraph.graphql index 08a37b12e1..0b1bdf1414 100644 --- a/examples/graphql/supergraph.graphql +++ b/examples/graphql/supergraph.graphql @@ -1,86 +1,121 @@ - schema - @core(feature: "https://specs.apollo.dev/core/v0.2"), - @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) -{ + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE -directive @join__field(graph: join__Graph, provides: join__FieldSet, requires: join__FieldSet) on FIELD_DEFINITION +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @join__graph(name: String!, url: String!) on ENUM_VALUE -directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} -directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on INTERFACE | OBJECT +scalar link__Import -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review @join__field(graph: REVIEWS) +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) } type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: ACCOUNTS, key: "upc", extension: true) @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") -{ + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + upc: String! + weight: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + price: Int + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) inStock: Boolean @join__field(graph: INVENTORY) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) reviews: [Review] @join__field(graph: REVIEWS) reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) } -type Query { +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { me: User @join__field(graph: ACCOUNTS) + recommendedProducts: [Product] @join__field(graph: ACCOUNTS) topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") -{ +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) + product: Product } type User - @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") -{ - id: ID! @join__field(graph: ACCOUNTS) + @join__type(graph: REVIEWS, key: "id") { + id: ID! name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) -} - -enum core__Purpose { - """ - `EXECUTION` features provide metadata necessary to for operation execution. - """ - EXECUTION - - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY -} - -scalar join__FieldSet - -enum join__Graph { - ACCOUNTS @join__graph(name: "accounts" url: "https://accounts.demo.starstuff.dev") - INVENTORY @join__graph(name: "inventory" url: "https://inventory.demo.starstuff.dev") - PRODUCTS @join__graph(name: "products" url: "https://products.demo.starstuff.dev") - REVIEWS @join__graph(name: "reviews" url: "https://reviews.demo.starstuff.dev") } diff --git a/examples/supergraph-sdl/rust/Cargo.toml b/examples/supergraph-sdl/rust/Cargo.toml index 065990a8ef..ead321ca05 100644 --- a/examples/supergraph-sdl/rust/Cargo.toml +++ b/examples/supergraph-sdl/rust/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] anyhow = "1" -apollo-compiler = "=1.0.0-beta.21" +apollo-compiler = "=1.0.0-beta.23" apollo-router = { path = "../../../apollo-router" } async-trait = "0.1" tower = { version = "0.4", features = ["full"] } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index dfbb4c9725..38603f704b 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -14,13 +14,13 @@ libfuzzer-sys = "0.4" apollo-compiler.workspace = true apollo-parser.workspace = true apollo-smith.workspace = true -env_logger = "0.10.2" +env_logger = "0.11.0" log = "0.4" reqwest = { workspace = true, features = ["json", "blocking"] } serde_json.workspace = true tokio.workspace = true # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.6.1+v2.9.0" +router-bridge = "=0.6.2+v2.9.1" [dev-dependencies] anyhow = "1" diff --git a/fuzz/subgraph/Cargo.toml b/fuzz/subgraph/Cargo.toml index 0e8ce3b5c8..99c60ef6bf 100644 --- a/fuzz/subgraph/Cargo.toml +++ b/fuzz/subgraph/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" axum = "0.6.20" async-graphql = "6" async-graphql-axum = "6" -env_logger = "0.10" +env_logger = "0.11" futures = "0.3.17" lazy_static = "1.4.0" log = "0.4.16" diff --git a/fuzz/supergraph-fed2.graphql b/fuzz/supergraph-fed2.graphql deleted file mode 100644 index f588155d30..0000000000 --- a/fuzz/supergraph-fed2.graphql +++ /dev/null @@ -1,141 +0,0 @@ -schema - @core(feature: "https://specs.apollo.dev/core/v0.2") - @core(feature: "https://specs.apollo.dev/join/v0.2", for: EXECUTION) { - query: Query -} - -directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT -directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - -directive @core( - feature: String! - as: String - for: core__Purpose -) repeatable on SCHEMA - -directive @join__field( - graph: join__Graph! - requires: join__FieldSet - provides: join__FieldSet - type: String - external: Boolean -) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION - -directive @join__graph(name: String!, url: String!) on ENUM_VALUE - -directive @join__implements( - graph: join__Graph! - interface: String! -) repeatable on OBJECT | INTERFACE - -directive @join__type( - graph: join__Graph! - key: join__FieldSet - extension: Boolean! = false - resolvable: Boolean! = true -) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR - -enum core__Purpose { - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY - - """ - `EXECUTION` features provide metadata necessary for operation execution. - """ - EXECUTION -} - -type DeliveryEstimates @join__type(graph: INVENTORY) { - estimatedDelivery: String - fastestDelivery: String -} - -scalar join__FieldSet - -enum join__Graph { - INVENTORY - @join__graph(name: "inventory", url: "http://localhost:4004/graphql") - PANDAS @join__graph(name: "pandas", url: "http://localhost:4002/graphql") - PRODUCTS @join__graph(name: "products", url: "http://localhost:4003/graphql") - USERS @join__graph(name: "users", url: "http://localhost:4001/graphql") -} - -type Panda @join__type(graph: PANDAS) { - name: ID! - favoriteFood: String -} - -type Product implements ProductItf & SkuItf - @join__implements(graph: INVENTORY, interface: "ProductItf") - @join__implements(graph: PRODUCTS, interface: "ProductItf") - @join__implements(graph: PRODUCTS, interface: "SkuItf") - @join__type(graph: INVENTORY, key: "id") - @join__type(graph: PRODUCTS, key: "id") - @join__type(graph: PRODUCTS, key: "sku package") - @join__type(graph: PRODUCTS, key: "sku variation { id }") { - id: ID! - dimensions: ProductDimension - @join__field(graph: INVENTORY, external: true) - @join__field(graph: PRODUCTS) - delivery(zip: String): DeliveryEstimates - @join__field(graph: INVENTORY, requires: "dimensions { size weight }") - sku: String @join__field(graph: PRODUCTS) - package: String @join__field(graph: PRODUCTS) - variation: ProductVariation @join__field(graph: PRODUCTS) - createdBy: User @join__field(graph: PRODUCTS) -} - -type ProductDimension - @join__type(graph: INVENTORY) - @join__type(graph: PRODUCTS) { - size: String - weight: Float -} - -interface ProductItf implements SkuItf - @join__implements(graph: PRODUCTS, interface: "SkuItf") - @join__type(graph: INVENTORY) - @join__type(graph: PRODUCTS) { - id: ID! - dimensions: ProductDimension - delivery(zip: String): DeliveryEstimates @join__field(graph: INVENTORY) - sku: String @join__field(graph: PRODUCTS) - package: String @join__field(graph: PRODUCTS) - variation: ProductVariation @join__field(graph: PRODUCTS) - createdBy: User @join__field(graph: PRODUCTS) -} - -type ProductVariation @join__type(graph: PRODUCTS) { - id: ID! -} - -type Query - @join__type(graph: INVENTORY) - @join__type(graph: PANDAS) - @join__type(graph: PRODUCTS) - @join__type(graph: USERS) { - allPandas: [Panda] @join__field(graph: PANDAS) - panda(name: ID!): Panda @join__field(graph: PANDAS) - allProducts: [ProductItf] @join__field(graph: PRODUCTS) - product(id: ID!): ProductItf @join__field(graph: PRODUCTS) -} - -enum ShippingClass @join__type(graph: INVENTORY) @join__type(graph: PRODUCTS) { - STANDARD - EXPRESS - OVERNIGHT -} - -interface SkuItf @join__type(graph: PRODUCTS) { - sku: String -} - -type User - @join__type(graph: PRODUCTS, key: "email") - @join__type(graph: USERS, key: "email") { - email: ID! - totalProductsCreated: Int - name: String @join__field(graph: USERS) -} diff --git a/fuzz/supergraph.graphql b/fuzz/supergraph.graphql index 63802405f1..112d8f88f0 100644 --- a/fuzz/supergraph.graphql +++ b/fuzz/supergraph.graphql @@ -1,86 +1,147 @@ schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") { + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { query: Query mutation: Mutation } -directive @core(feature: String!) repeatable on SCHEMA - directive @join__field( graph: join__Graph requires: join__FieldSet provides: join__FieldSet -) on FIELD_DEFINITION + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE directive @join__type( graph: join__Graph! key: join__FieldSet -) repeatable on OBJECT | INTERFACE + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR -directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA -directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT -directive @deprecated( - reason: String = "No longer supported" -) on FIELD_DEFINITION | ENUM_VALUE +scalar join__FieldSet +scalar link__Import -# Uncomment if you want to reproduce the bug with the order of skip/include directives -# directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT -# directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY -scalar join__FieldSet + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} enum join__Graph { - ACCOUNTS @join__graph(name: "accounts", url: "http://subgraphs:4001/graphql") INVENTORY - @join__graph(name: "inventory", url: "http://subgraphs:4004/graphql") - PRODUCTS @join__graph(name: "products", url: "http://subgraphs:4003/graphql") - REVIEWS @join__graph(name: "reviews", url: "http://subgraphs:4002/graphql") + @join__graph(name: "inventory", url: "http://localhost:4004/graphql") + PANDAS @join__graph(name: "pandas", url: "http://localhost:4002/graphql") + PRODUCTS @join__graph(name: "products", url: "http://localhost:4003/graphql") + USERS @join__graph(name: "users", url: "http://localhost:4001/graphql") +} + +type DeliveryEstimates @join__type(graph: INVENTORY) { + estimatedDelivery: String + fastestDelivery: String +} + +type Panda @join__type(graph: PANDAS) { + name: ID! + favoriteFood: String +} + +type Product implements ProductItf & SkuItf + @join__implements(graph: INVENTORY, interface: "ProductItf") + @join__implements(graph: PRODUCTS, interface: "ProductItf") + @join__implements(graph: PRODUCTS, interface: "SkuItf") + @join__type(graph: INVENTORY, key: "id") + @join__type(graph: PRODUCTS, key: "id") + @join__type(graph: PRODUCTS, key: "sku package") + @join__type(graph: PRODUCTS, key: "sku variation { id }") { + id: ID! + dimensions: ProductDimension + @join__field(graph: INVENTORY, external: true) + @join__field(graph: PRODUCTS) + delivery(zip: String): DeliveryEstimates + @join__field(graph: INVENTORY, requires: "dimensions { size weight }") + sku: String @join__field(graph: PRODUCTS) + package: String @join__field(graph: PRODUCTS) + variation: ProductVariation @join__field(graph: PRODUCTS) + createdBy: User @join__field(graph: PRODUCTS) +} + +type ProductDimension + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) { + size: String + weight: Float +} + +interface ProductItf implements SkuItf + @join__implements(graph: PRODUCTS, interface: "SkuItf") + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) { + id: ID! + dimensions: ProductDimension + delivery(zip: String): DeliveryEstimates @join__field(graph: INVENTORY) + sku: String @join__field(graph: PRODUCTS) + package: String @join__field(graph: PRODUCTS) + variation: ProductVariation @join__field(graph: PRODUCTS) + createdBy: User @join__field(graph: PRODUCTS) } -type Mutation { - createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) - createReview(body: String, id: ID!, upc: ID!): Review - @join__field(graph: REVIEWS) +type ProductVariation @join__type(graph: PRODUCTS) { + id: ID! } -type Product - @join__owner(graph: PRODUCTS) - @join__type(graph: PRODUCTS, key: "upc") - @join__type(graph: INVENTORY, key: "upc") - @join__type(graph: REVIEWS, key: "upc") { - inStock: Boolean @join__field(graph: INVENTORY) - name: String @join__field(graph: PRODUCTS) - price: Int @join__field(graph: PRODUCTS) - reviews: [Review] @join__field(graph: REVIEWS) - reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) - shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") - upc: String! @join__field(graph: PRODUCTS) - weight: Int @join__field(graph: PRODUCTS) +type Query + @join__type(graph: INVENTORY) + @join__type(graph: PANDAS) + @join__type(graph: PRODUCTS) + @join__type(graph: USERS) { + allPandas: [Panda] @join__field(graph: PANDAS) + panda(name: ID!): Panda @join__field(graph: PANDAS) + allProducts: [ProductItf] @join__field(graph: PRODUCTS) + product(id: ID!): ProductItf @join__field(graph: PRODUCTS) } -type Query { - me: User @join__field(graph: ACCOUNTS) - topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) +enum ShippingClass @join__type(graph: INVENTORY) @join__type(graph: PRODUCTS) { + STANDARD + EXPRESS + OVERNIGHT } -type Review - @join__owner(graph: REVIEWS) - @join__type(graph: REVIEWS, key: "id") { - author: User @join__field(graph: REVIEWS, provides: "username") - body: String @join__field(graph: REVIEWS) - id: ID! @join__field(graph: REVIEWS) - product: Product @join__field(graph: REVIEWS) +interface SkuItf @join__type(graph: PRODUCTS) { + sku: String } type User - @join__owner(graph: ACCOUNTS) - @join__type(graph: ACCOUNTS, key: "id") - @join__type(graph: REVIEWS, key: "id") { - id: ID! @join__field(graph: ACCOUNTS) - name: String @join__field(graph: ACCOUNTS) - reviews: [Review] @join__field(graph: REVIEWS) - username: String @join__field(graph: ACCOUNTS) + @join__type(graph: PRODUCTS, key: "email") + @join__type(graph: USERS, key: "email") { + email: ID! + totalProductsCreated: Int + name: String @join__field(graph: USERS) } diff --git a/helm/chart/router/Chart.yaml b/helm/chart/router/Chart.yaml index 50f1886874..7a1c6d615a 100644 --- a/helm/chart/router/Chart.yaml +++ b/helm/chart/router/Chart.yaml @@ -20,10 +20,10 @@ type: application # so it matches the shape of our release process and release automation. # By proxy of that decision, this version uses SemVer 2.0.0, though the prefix # of "v" is not included. -version: 1.54.0 +version: 1.55.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.54.0" +appVersion: "v1.55.0" diff --git a/helm/chart/router/README.md b/helm/chart/router/README.md index 39e6238055..fb09765b10 100644 --- a/helm/chart/router/README.md +++ b/helm/chart/router/README.md @@ -2,7 +2,7 @@ [router](https://github.com/apollographql/router) Rust Graph Routing runtime for Apollo Federation -![Version: 1.54.0](https://img.shields.io/badge/Version-1.54.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.54.0](https://img.shields.io/badge/AppVersion-v1.54.0-informational?style=flat-square) +![Version: 1.55.0](https://img.shields.io/badge/Version-1.55.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.55.0](https://img.shields.io/badge/AppVersion-v1.55.0-informational?style=flat-square) ## Prerequisites @@ -11,7 +11,7 @@ ## Get Repo Info ```console -helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.54.0 +helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0 ``` ## Install Chart @@ -19,7 +19,7 @@ helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.54.0 **Important:** only helm3 is supported ```console -helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.54.0 --values my-values.yaml +helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.55.0 --values my-values.yaml ``` _See [configuration](#configuration) below._ diff --git a/licenses.html b/licenses.html index e50ef97a86..e415d536c4 100644 --- a/licenses.html +++ b/licenses.html @@ -44,8 +44,8 @@

Third Party Licenses

Overview of licenses:

    -
  • Apache License 2.0 (432)
  • -
  • MIT License (146)
  • +
  • Apache License 2.0 (448)
  • +
  • MIT License (155)
  • BSD 3-Clause "New" or "Revised" License (11)
  • ISC License (8)
  • Mozilla Public License 2.0 (5)
  • @@ -65,6 +65,7 @@

    Used by:

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

    Used by:

    Apache License 2.0

    Used by:

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

    Apache License 2.0

    +

    Used by:

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

    Used by:

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

Used by:

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

    Used by:

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

    Used by:

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

    Used by:

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

    Used by:

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

    Used by:

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

    Used by:

    Apache License 2.0

    Used by:

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

    Used by:

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

    Used by:

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

    Used by:

    Apache License 2.0

    Used by:

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

    Apache License 2.0

    -

    Used by:

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

    Used by:

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

    Apache License 2.0

    +

    Used by:

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

    Apache License 2.0

    +

    Used by:

    + +
    ../../LICENSE-APACHE
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

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

    Apache License 2.0

    Used by:

  • + +
  • +

    Apache License 2.0

    +

    Used by:

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

    Apache License 2.0

    +

    Used by:

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

    Apache License 2.0

    +

    Used by:

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

    Apache License 2.0

    +

    Used by:

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

    Apache License 2.0

    +

    Used by:

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

    Apache License 2.0

    +

    Used by:

    + +
    MIT or Apache-2.0
     
  • @@ -12413,36 +12538,6 @@

    Used by:

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

    ISC License

    -

    Used by:

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

    ISC License

    Used by:

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

    Used by:

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

    MIT License

    +

    Used by:

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

    Used by:

    MIT License

    Used by:

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

      Used by:

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

      Used by:

      MIT License

      Used by:

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

      MIT License

      +

      Used by:

      + @@ -14589,7 +14773,6 @@

      Used by:

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

    Used by:

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

    MIT License

    +

    Used by:

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

    Used by:

    Mozilla Public License 2.0

    Used by:

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

    Used by:

    Mozilla Public License 2.0

    Used by:

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

    Used by:

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

    Mozilla Public License 2.0

    +

    Used by:

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

    Used by:

    UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE
     
    -See Terms of Use <https://www.unicode.org/copyright.html>
    -for definitions of Unicode Inc.’s Data Files and Software.
    +Unicode Data Files include all data files under the directories http://www.unicode.org/Public/, http://www.unicode.org/reports/, http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and http://www.unicode.org/utility/trac/browser/.
     
    -NOTICE TO USER: Carefully read the following legal agreement.
    -BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S
    -DATA FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"),
    -YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
    -TERMS AND CONDITIONS OF THIS AGREEMENT.
    -IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE
    -THE DATA FILES OR SOFTWARE.
    +Unicode Data Files do not include PDF online code charts under the directory http://www.unicode.org/Public/.
    +
    +Software includes any source code published in the Unicode Standard or under the directories http://www.unicode.org/Public/, http://www.unicode.org/reports/, http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and http://www.unicode.org/utility/trac/browser/.
    +
    +NOTICE TO USER: Carefully read the following legal agreement. BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S DATA FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
     
     COPYRIGHT AND PERMISSION NOTICE
     
    -Copyright © 1991-2022 Unicode, Inc. All rights reserved.
    -Distributed under the Terms of Use in https://www.unicode.org/copyright.html.
    +Copyright © 1991-2016 Unicode, Inc. All rights reserved. Distributed under the Terms of Use in http://www.unicode.org/copyright.html.
     
    -Permission is hereby granted, free of charge, to any person obtaining
    -a copy of the Unicode data files and any associated documentation
    -(the "Data Files") or Unicode software and any associated documentation
    -(the "Software") to deal in the Data Files or Software
    -without restriction, including without limitation the rights to use,
    -copy, modify, merge, publish, distribute, and/or sell copies of
    -the Data Files or Software, and to permit persons to whom the Data Files
    -or Software are furnished to do so, provided that either
    -(a) this copyright and permission notice appear with all copies
    -of the Data Files or Software, or
    -(b) this copyright and permission notice appear in associated
    -Documentation.
    -
    -THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF
    -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
    -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    -NONINFRINGEMENT OF THIRD PARTY RIGHTS.
    -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS
    -NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL
    -DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
    -DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
    -TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
    -PERFORMANCE OF THE DATA FILES OR SOFTWARE.
    -
    -Except as contained in this notice, the name of a copyright holder
    -shall not be used in advertising or otherwise to promote the sale,
    -use or other dealings in these Data Files or Software without prior
    -written authorization of the copyright holder.
    +Permission is hereby granted, free of charge, to any person obtaining a copy of the Unicode data files and any associated documentation (the "Data Files") or Unicode software and any associated documentation (the "Software") to deal in the Data Files or Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the Data Files or Software, and to permit persons to whom the Data Files or Software are furnished to do so, provided that either
    +
    +     (a) this copyright and permission notice appear with all copies of the Data Files or Software, or
    +     (b) this copyright and permission notice appear in associated Documentation.
    +
    +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA FILES OR SOFTWARE.
    +
    +Except as contained in this notice, the name of a copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in these Data Files or Software without prior written authorization of the copyright holder.
     
  • diff --git a/scripts/install.sh b/scripts/install.sh index 0447b4c144..f03b383d8d 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -11,7 +11,7 @@ BINARY_DOWNLOAD_PREFIX="https://github.com/apollographql/router/releases/downloa # Router version defined in apollo-router's Cargo.toml # Note: Change this line manually during the release steps. -PACKAGE_VERSION="v1.54.0" +PACKAGE_VERSION="v1.55.0" download_binary() { downloader --check diff --git a/xtask/Cargo.lock b/xtask/Cargo.lock index 819a7106ac..0a1c01d258 100644 --- a/xtask/Cargo.lock +++ b/xtask/Cargo.lock @@ -128,6 +128,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -567,9 +573,9 @@ dependencies = [ [[package]] name = "graphql_client" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cdf7b487d864c2939b23902291a5041bc4a84418268f25fda1c8d4e15ad8fa" +checksum = "a50cfdc7f34b7f01909d55c2dcb71d4c13cbcbb4a1605d6c8bd760d654c1144b" dependencies = [ "graphql_query_derive", "reqwest", @@ -579,9 +585,9 @@ dependencies = [ [[package]] name = "graphql_client_codegen" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a40f793251171991c4eb75bd84bc640afa8b68ff6907bc89d3b712a22f700506" +checksum = "5e27ed0c2cf0c0cc52c6bcf3b45c907f433015e580879d14005386251842fb0a" dependencies = [ "graphql-introspection-query", "graphql-parser", @@ -596,9 +602,9 @@ dependencies = [ [[package]] name = "graphql_query_derive" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bda454f3d313f909298f626115092d348bc231025699f557b27e248475f48c" +checksum = "83febfa838f898cfa73dfaa7a8eb69ff3409021ac06ee94cfb3d622f6eeb1a97" dependencies = [ "graphql_client_codegen", "proc-macro2", @@ -795,9 +801,9 @@ checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -900,11 +906,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.49.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1150,7 +1156,7 @@ version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -1265,7 +1271,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64", + "base64 0.21.7", ] [[package]] @@ -2114,7 +2120,7 @@ name = "xtask" version = "1.5.0" dependencies = [ "anyhow", - "base64", + "base64 0.22.1", "camino", "cargo_metadata", "chrono", @@ -2145,9 +2151,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zip" diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 7efbefff1b..b57c3a8759 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -20,17 +20,17 @@ chrono = { version = "0.4.34", default-features = false, features = ["clock"] } console = "0.15.8" dialoguer = "0.11.0" flate2 = "1" -graphql_client = { version = "0.13.0", features = ["reqwest-rustls"] } -itertools = "0.12.1" +graphql_client = { version = "0.14.0", features = ["reqwest-rustls"] } +itertools = "0.13.0" libc = "0.2" memorable-wordlist = "0.1.7" -nu-ansi-term = "0.49" +nu-ansi-term = "0.50" once_cell = "1" regex = "1.10.3" reqwest = { version = "0.11", default-features = false, features = [ "blocking", "rustls-tls", - "rustls-tls-native-roots", + "rustls-tls-native-roots" ] } serde = { version = "1.0.197", features = ["derive"] } serde_json = "1" @@ -43,7 +43,7 @@ walkdir = "2.4.0" xshell = "0.2.6" [target.'cfg(target_os = "macos")'.dependencies] -base64 = "0.21" +base64 = "0.22" zip = { version = "0.6", default-features = false } [dev-dependencies]