apollographql / federation

🌐  Build and scale a single data graph across multiple services with Apollo's federation gateway.
https://apollographql.com/docs/federation/
Other
661 stars 248 forks source link

Union merge input types and enums instead of intersecting #2180

Open johnkm516 opened 1 year ago

johnkm516 commented 1 year ago

I am trying to adopt Federation 2 but ran into what I think is a huge problem; merging input types and enums. As it says in the docs, input types and field arguments use the intersection strategy rather than the union strategy because :

Composition always uses the intersection strategy to merge input types and field arguments. This ensures that the router never passes an argument to a subgraph that doesn't define that argument.

I am having trouble understanding why though? Isn't it the router's job to make sure the arguments route to the correct subgraph in the first place? The same way it does for type compositions?

Consider two subgraphs Product and Inventory as per the docs :

image

From these models, intuitively it makes sense to me for each of the subgraphs to declare their own corresponding inputs :

Product subgraph

  input ProductCreateInput {
    id: Int!
    name: String!
    price: Int!
  }

Inventory subgraph

  input ProductCreateInput {
    id: Int!
    inStock: Boolean!
  }

The expectation would be that the input type is merged using the union strategy; the supergraph SDL keeping track of which input fields belong to which subgraph, and the gateway splitting the input argument fields that belong to each subgraph before routing the input data, therefore ensuring the router never passes an argument to a subgraph that doesn't define that argument.

By doing a intersecting merge for input types and enums I feel like there is a huge disconnect between how types are declared and composed vs how input / enums are declared and composed. Input types especially almost directly correspond to the object type, it doesn't really make sense to me why Input types are merged using the intersection strategy. This works when you're building resolvers for querying with keyFields, but for much more complicated queries dealing with collections where the input arguments could aggregate, filter, and sort, coding and querying the arguments get really complicated. It also forces me to declare multiple different input type names like this :

  input ProductCreateInput_Product {
    name: String!
    price: Int!
  }

Inventory subgraph

  input ProductCreateInput_Inventory {
    inStock: Boolean!
  }

and query them two separate times. For example, if I wanted to create a product with a specific name and also add the inStock field, I would have to query ProductCreateInput_Product then query ProductCreateInput_Inventory. This is just a basic example but I could extend input types to far more complicated arguments like :

Product subgraph

input ProductWhereInput {
  AND: [ProductWhereInput!]
  OR: [ProductWhereInput!]
  NOT: [ProductWhereInput!]
  id: IntFilter
  name: StringFilter
  price:  IntFilter
}

export interface IntFilter {
  equals?: number;
  in?: number[];
  notIn?: number[];
  lt?: number;
  lte?: number;
  gt?: number;
  gte?: number;
  not?: NestedIntFilter;
}

export interface StringFilter {
  equals?: string;
  in?: string[];
  notIn?: string[];
  contains?: string;
  startsWith?: string;
  endsWith?: string;
}

Inventory subgraph

input ProductWhereInput {
  AND: [ProductWhereInput!]
  OR: [ProductWhereInput!]
  NOT: [ProductWhereInput!]
  id: IntFilter
  inStock: Boolean!
}

export interface IntFilter {
  equals?: number;
  in?: number[];
  notIn?: number[];
  lt?: number;
  lte?: number;
  gt?: number;
  gte?: number;
  not?: NestedIntFilter;
}

in which case it gets more and more difficult to deal with input types. Do I just have a fundamental misunderstanding of how to implement subgraphs? Are there limitations that has made input types / enum compositions using the union strategy unviable?

robross0606 commented 1 year ago

Part of the problem here is how extends interacts with intersection composition on a federated schema. If two services take a common input base type and extend them, the extension happens before composition. This is very obvious with something like a shared schema for sorting or filtering

Shared input base type:

input FieldSpecificationInput {
  common: CommonFieldSpec
}

Subgraph A extended type:

extend input FieldSpecificationInput {
  graphA: GraphAFieldSpec
}

Subgraph B extended type:

extend input FieldSpecificationInput {
  graphB: GraphBFieldSpec
}

If you only put Graph A in your federation, the resulting composed type is:

input FieldSpecificationInput {
  common: CommonFieldSpec
  graphA: GraphAFieldSpec
}

But the second you add Graph B to your federation, the resulting composed type is:

input FieldSpecificationInput {
  common: CommonFieldSpec
}

The extension is effectively cancelled out by intersecting composition. There doesn't seem to be any current means of changing this behavior and the two strategies of extension and composition are in direct contradiction to each other. Not only that, but intersection as a "smart" pattern just means you're quietly removing API endpoints without any errors or warnings during composition. Intersection composition means that one subgraph can cause another subgraph's contributions to be removed. I'm not sure how that's a desirable pattern for anyone or could be considered "smart".