apollographql / router

A configurable, high-performance routing runtime for Apollo Federation 🚀
https://www.apollographql.com/docs/router/
Other
813 stars 271 forks source link

Query planner fails when using @skip or @include directives with input params #6196

Open nathanmarcos opened 3 weeks ago

nathanmarcos commented 3 weeks ago

Describe the bug

Hello, I think we just hit an edge case where the query planner fails to create a plan for a query that uses @skip or @include directives for a field that has @requires and also accepts inputs.

To Reproduce

Steps to reproduce the behavior:

  1. We will need 2 subgraphs, in this example, product and shipping.

Product Schema:

  @link(
    url: "https://specs.apollo.dev/federation/v2.3"
    import: ["@key"]
  )

  type Query {
    allProducts: [Product!]
  }

  type Product @key(fields: "id") {
    id: ID!
    dimensions(unitType: UnitType): ProductDimensions
  }

  type ProductDimensions {
    size: Int
    weight: Int
  }

  enum UnitType {
    METRIC
    IMPERIAL
  }

Shipping Schema:

  @link(
    url: "https://specs.apollo.dev/federation/v2.3"
    import: ["@key", "@external", "@requires"]
  )

  extend type Product @key(fields: "id") {
    id: ID!
    dimensions(unitType: UnitType): ProductDimensions @external
    shippingEstimate: Int @requires(fields: "dimensions { size weight }")
  }

  extend type ProductDimensions {
    size: Int @external
    weight: Int @external
  }

  enum UnitType {
    METRIC
    IMPERIAL
  }

The supergraph:

schema
  @link(url: "https://specs.apollo.dev/link/v1.0")
  @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
{
  query: Query
}

directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE

directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION

directive @join__graph(name: String!, url: String!) on ENUM_VALUE

directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE

directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR

directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION

directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA

scalar join__FieldSet

enum join__Graph {
  PRODUCT @join__graph(name: "product", url: "http://localhost:4010")
  SHIPPING @join__graph(name: "shipping", url: "http://localhost:4011")
}

scalar link__Import

enum link__Purpose {
  """
  `SECURITY` features provide metadata necessary to securely resolve fields.
  """
  SECURITY

  """
  `EXECUTION` features provide metadata necessary for operation execution.
  """
  EXECUTION
}

type Product
  @join__type(graph: PRODUCT, key: "id")
  @join__type(graph: SHIPPING, key: "id", extension: true)
{
  id: ID!
  dimensions(unitType: UnitType): ProductDimensions @join__field(graph: PRODUCT) @join__field(graph: SHIPPING, external: true)
  shippingEstimate: Int @join__field(graph: SHIPPING, requires: "dimensions { size weight }")
}

type ProductDimensions
  @join__type(graph: PRODUCT)
  @join__type(graph: SHIPPING)
{
  size: Int @join__field(graph: PRODUCT) @join__field(graph: SHIPPING, external: true)
  weight: Int @join__field(graph: PRODUCT) @join__field(graph: SHIPPING, external: true)
}

type Query
  @join__type(graph: PRODUCT)
  @join__type(graph: SHIPPING)
{
  allProducts: [Product!] @join__field(graph: PRODUCT)
}

enum UnitType
  @join__type(graph: PRODUCT)
  @join__type(graph: SHIPPING)
{
  METRIC @join__enumValue(graph: PRODUCT) @join__enumValue(graph: SHIPPING)
  IMPERIAL @join__enumValue(graph: PRODUCT) @join__enumValue(graph: SHIPPING)
}
  1. Shoot the following query to router:
    
    query AllProducts {
    allProducts {
    id
    dimensions(unitType: IMPERIAL) {
      size
      weight
    }
    shippingEstimate @include(if: true)
    }
    }

3. You will get the following error:
- When using the JS query planner: 

"Fields \"dimensions\" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional."

- When using the Rust query planner: 

"value retrieval failed: Federation error: An internal error has occurred, please report this bug to Apollo.\n\nDetails: Query planning produced an invalid subgraph operation.\nError: operation must not provide conflicting field arguments for the same name dimensions\n"


## Expected behavior
Ideally this should not fail. Also, when removing either the `unitType` input or the `@include` directive from the query it works just fine.

## Desktop (please complete the following information):
 - OS: macOS Sonoma 14.6.1
 - Router Version: v1.57.0

## Off-topic but also related (Maybe this deserves another dedicated issue)
We would love to be able to tell router to use the `unitType` input (when available in the query) when fetching the field to fulfill the the `@requires` directive, example:
```graphql
  extend type Product @key(fields: "id") {
    id: ID!
    dimensions(unitType: UnitType): ProductDimensions @external
    shippingEstimate: Int @requires(fields: "dimensions(unitType: $unitType) { size weight }")
    Inject it here ---------------------------------------------------^
  }

So both requested dimensions field and the shippingEstimate would use the same input defined by the client. Currently we can't guarantee that the dimensions used to calculate the shippingEstimate will use the correct unitType.