From 8ee78ad2fc03a712329c27e973ae65807c2cfbad Mon Sep 17 00:00:00 2001 From: Vince Foley Date: Sun, 15 Sep 2019 16:16:44 -0700 Subject: [PATCH 1/7] Add InputUnion problem statement --- rfcs/InputUnion.md | 115 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/rfcs/InputUnion.md b/rfcs/InputUnion.md index 30f1694c2..d34f13a61 100644 --- a/rfcs/InputUnion.md +++ b/rfcs/InputUnion.md @@ -18,8 +18,119 @@ To help bring this idea to reality, you can contribute through two channels: ## Problem Statement -TODO: -* [ ] Describe at a high level the problem that Input Union is trying to solve. +GraphQL currently provides abstract types that operate on **Object** types, but they aren't allowed on **Input** types. + +Over the years there have been numerous proposals from the community to add an abstract input type. Without an abstract type, users have had to resort to a handful of work-arounds to model their domains. These work-arounds lead to schemas that aren't as expressive as they could be, and even schemas that model data differently across queries & mutations. + +Let's imagine an animal shelter for our example. When querying for a list of the animals, it's easy to see how abstract types are useful - we can get data specific to the type of the animal easily. + +```graphql +{ + animalShelter(location: "Portland, OR") { + animals { + __typename + name + age + ... on Cat { livesLeft } + ... on Dog { breed } + ... on Snake { venom } + } + } +} +``` + +However, when we want to submit data, we can't use an `interface` or `union`, so we must model around that. + +One technique commonly used to is a **Tagged union** pattern. This essentially boils down to a "wrapper" input that isolates each type into it's own field. The field name takes on the convention of representing the type. + +```graphql +mutation { + logAnimalDropOff( + location: "Portland, OR" + animals: [ + {cat: {name: "Buster", age: 3, livesLeft: 7}} + ] + ) +} +``` + +Unfortunately, this opens up a set of problems, since the Tagged union input type actually contains many fields, any of which could be submitted. + +```graphql +input AnimalDropOff { + cat: Cat + dog: Dog + snake: Snake +} +``` + +This leads to non-sensical mutations that are totally valid, like an animal that is both a `Cat` and a `Dog` + +```graphql +mutation { + logAnimalDropOff( + location: "Portland, OR" + animals: [ + { + cat: {name: "Buster", age: 3, livesLeft: 7}, + dog: {name: "Ripple", age: 2, breed: WHIPPET} + } + ] + ) +} +``` + +In addition, relying on this layer of abstraction means that this domain is modeled differently across input & output. This puts a large burden on the developer interacting with the API, as they have to code against this. + +```json +// JSON structure returned from a query +{ + "animals": [ + {"__typename": "Cat", "name": "Ruby", "age": 2, "livesLeft": 9} + {"__typename": "Snake", "name": "Monty", "age": 13, "venom": "POISON"} + ] +} +``` + +```json +// JSON structure submitted to a mutation +{ + "animals": [ + {"cat": {"name": "Ruby", "age": 2, "livesLeft": 9}}, + {"snake": {"name": "Monty", "age": 13, "venom": "POISON"}} + ] +} +``` + +Another common approach is to provide a unique mutation for every type. So you'd have a `logCatDropOff` and `logDogDropOff` and `logSnakeDropOff`. That removes the potential for modeling non-sensical situations, but it explodes the number of mutations in an API. + +These workarounds only get worse at scale. Real world APIs can have dozens if not hundreds of possible types. + +The idea of the **Input Union** is to bring an abstract type to inputs. This would enable us to model our domain as elegantly in input as we can in output. + +```graphql +mutation { + logAnimalDropOff( + location: "Portland, OR" + animals: [ + {name: "Buster", age: 3, livesLeft: 7}, + {name: "Ripple", age: 2, __typename: "Dog"}, + ] + ) +} +``` + +```graphql +inputunion AnimalDropOff = Dog | Cat | Snake + +type Mutation { + logAnimalDropOff(location: String, animals: [AnimalDropOff]): Boolean +} +``` + +In this mutation, we encounter the main challenge of the **Input Union** - we need to determine the correct type of the data submitted. In a query, we have the `... on Cat` fragment which explicitly identifies the concrete type of the interface or union. + +A wide variety of solutions have been explored by the community, and they are outlined in detail in this document under [Possible Solutions](#Possible-Solutions). ## Use Cases From d770ffeb009aea399abe0167c479d871fbf1f44d Mon Sep 17 00:00:00 2001 From: Vince Foley <39946+binaryseed@users.noreply.github.com> Date: Mon, 16 Sep 2019 15:45:18 -0700 Subject: [PATCH 2/7] Apply suggestions from code review Co-Authored-By: Benjie Gillam --- rfcs/InputUnion.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rfcs/InputUnion.md b/rfcs/InputUnion.md index d34f13a61..5ac75ef64 100644 --- a/rfcs/InputUnion.md +++ b/rfcs/InputUnion.md @@ -20,7 +20,7 @@ To help bring this idea to reality, you can contribute through two channels: GraphQL currently provides abstract types that operate on **Object** types, but they aren't allowed on **Input** types. -Over the years there have been numerous proposals from the community to add an abstract input type. Without an abstract type, users have had to resort to a handful of work-arounds to model their domains. These work-arounds lead to schemas that aren't as expressive as they could be, and even schemas that model data differently across queries & mutations. +Over the years there have been numerous proposals from the community to add an abstract input type. Without an abstract type, users have had to resort to a handful of work-arounds to model their domains. These work-arounds have led to schemas that aren't as expressive as they could be, and schemas where some mutations that would be expected to reflect queries have instead been modeled differently by necessity. Let's imagine an animal shelter for our example. When querying for a list of the animals, it's easy to see how abstract types are useful - we can get data specific to the type of the animal easily. @@ -41,7 +41,7 @@ Let's imagine an animal shelter for our example. When querying for a list of the However, when we want to submit data, we can't use an `interface` or `union`, so we must model around that. -One technique commonly used to is a **Tagged union** pattern. This essentially boils down to a "wrapper" input that isolates each type into it's own field. The field name takes on the convention of representing the type. +One technique commonly used to is a **tagged union** pattern. This essentially boils down to a "wrapper" input that isolates each type into it's own field. The field name takes on the convention of representing the type. ```graphql mutation { @@ -64,7 +64,7 @@ input AnimalDropOff { } ``` -This leads to non-sensical mutations that are totally valid, like an animal that is both a `Cat` and a `Dog` +This allows non-sensical mutations to pass GraphQL validation, for example representing an animal that is both a `Cat` and a `Dog`. ```graphql mutation { @@ -80,7 +80,7 @@ mutation { } ``` -In addition, relying on this layer of abstraction means that this domain is modeled differently across input & output. This puts a large burden on the developer interacting with the API, as they have to code against this. +In addition, relying on this layer of abstraction means that this domain must be modelled differently across input & output. This can put a larger burden on the developer interacting with the schema, both in terms of lines of code and complexity. ```json // JSON structure returned from a query @@ -102,11 +102,11 @@ In addition, relying on this layer of abstraction means that this domain is mode } ``` -Another common approach is to provide a unique mutation for every type. So you'd have a `logCatDropOff` and `logDogDropOff` and `logSnakeDropOff`. That removes the potential for modeling non-sensical situations, but it explodes the number of mutations in an API. +Another common approach is to provide a unique mutation for every type. A schema employing this technique might have `logCatDropOff`, `logDogDropOff` and `logSnakeDropOff` mutations. This removes the potential for modeling non-sensical situations, but it explodes the number of mutations in a schema, making the schema less accessible. These workarounds only get worse at scale. Real world APIs can have dozens if not hundreds of possible types. -The idea of the **Input Union** is to bring an abstract type to inputs. This would enable us to model our domain as elegantly in input as we can in output. +The idea of the **Input Union** is to bring an abstract type to inputs. This would enable us to model situations where an input may be of different types in a type-safe and elegant manner, like we can with outputs. ```graphql mutation { @@ -130,7 +130,7 @@ type Mutation { In this mutation, we encounter the main challenge of the **Input Union** - we need to determine the correct type of the data submitted. In a query, we have the `... on Cat` fragment which explicitly identifies the concrete type of the interface or union. -A wide variety of solutions have been explored by the community, and they are outlined in detail in this document under [Possible Solutions](#Possible-Solutions). +A wide variety of solutions have been explored by the community, and they are outlined in detail in this document under [Possible Solutions](#Possible-Solutions). In the example above we employed a `__typename` field to indicate the type of animal, but this is only one possible solution. ## Use Cases From b3d7c2173ecde05b2f6f93da9e173012fdd2a3e2 Mon Sep 17 00:00:00 2001 From: Vince Foley Date: Mon, 16 Sep 2019 19:33:08 -0700 Subject: [PATCH 3/7] clarify --- rfcs/InputUnion.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rfcs/InputUnion.md b/rfcs/InputUnion.md index 5ac75ef64..0eb03f13a 100644 --- a/rfcs/InputUnion.md +++ b/rfcs/InputUnion.md @@ -104,9 +104,9 @@ In addition, relying on this layer of abstraction means that this domain must be Another common approach is to provide a unique mutation for every type. A schema employing this technique might have `logCatDropOff`, `logDogDropOff` and `logSnakeDropOff` mutations. This removes the potential for modeling non-sensical situations, but it explodes the number of mutations in a schema, making the schema less accessible. -These workarounds only get worse at scale. Real world APIs can have dozens if not hundreds of possible types. +These workarounds only get worse at scale. Real world GraphQL schemas can have dozens if not hundreds of possible types for a single `Interface` or `Union`. -The idea of the **Input Union** is to bring an abstract type to inputs. This would enable us to model situations where an input may be of different types in a type-safe and elegant manner, like we can with outputs. +The goal of the **Input Union** is to bring an abstract type to inputs. This would enable us to model situations where an input may be of different types in a type-safe and elegant manner, like we can with outputs. ```graphql mutation { From 15ccaedca78aa278cca7e55b514a7015b5d435d6 Mon Sep 17 00:00:00 2001 From: Vince Foley Date: Mon, 16 Sep 2019 19:54:27 -0700 Subject: [PATCH 4/7] split problem statement and sketch --- rfcs/InputUnion.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/rfcs/InputUnion.md b/rfcs/InputUnion.md index 0eb03f13a..651350094 100644 --- a/rfcs/InputUnion.md +++ b/rfcs/InputUnion.md @@ -18,9 +18,13 @@ To help bring this idea to reality, you can contribute through two channels: ## Problem Statement -GraphQL currently provides abstract types that operate on **Object** types, but they aren't allowed on **Input** types. +GraphQL currently provides polymorphic types that enable schema authors to model complex **Object** types that have multiple shapes while remaining type-safe, but lacks an equivilant capability for **Input** types. -Over the years there have been numerous proposals from the community to add an abstract input type. Without an abstract type, users have had to resort to a handful of work-arounds to model their domains. These work-arounds have led to schemas that aren't as expressive as they could be, and schemas where some mutations that would be expected to reflect queries have instead been modeled differently by necessity. +Over the years there have been numerous proposals from the community to add a polymorphic input type. Without such a type, schema authors have resorted to a handful of work-arounds to model their domains. These work-arounds have led to schemas that aren't as expressive as they could be, and schemas where mutations that ideally mirror queries are forced to be modeled differently. + +## Problem Sketch + +To understand the problem space a little more, we'll sketch out an example that explores a domain from the perspective of a Query and a Mutation. However, it's important to note that the problem is not limited to mutations, since `Input` types are used in field arguments for any GraphQL operation type. Let's imagine an animal shelter for our example. When querying for a list of the animals, it's easy to see how abstract types are useful - we can get data specific to the type of the animal easily. @@ -106,7 +110,7 @@ Another common approach is to provide a unique mutation for every type. A schema These workarounds only get worse at scale. Real world GraphQL schemas can have dozens if not hundreds of possible types for a single `Interface` or `Union`. -The goal of the **Input Union** is to bring an abstract type to inputs. This would enable us to model situations where an input may be of different types in a type-safe and elegant manner, like we can with outputs. +The goal of the **Input Union** is to bring a polymorphic type to Inputs. This would enable us to model situations where an input may be of different types in a type-safe and elegant manner, like we can with outputs. ```graphql mutation { From bed028ebf0c161afe33a1ed97712c195091b3e77 Mon Sep 17 00:00:00 2001 From: Vince Foley Date: Wed, 18 Sep 2019 09:33:02 -0700 Subject: [PATCH 5/7] add input where needed --- rfcs/InputUnion.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rfcs/InputUnion.md b/rfcs/InputUnion.md index 651350094..30609909d 100644 --- a/rfcs/InputUnion.md +++ b/rfcs/InputUnion.md @@ -61,10 +61,10 @@ mutation { Unfortunately, this opens up a set of problems, since the Tagged union input type actually contains many fields, any of which could be submitted. ```graphql -input AnimalDropOff { - cat: Cat - dog: Dog - snake: Snake +input AnimalDropOffInput { + cat: CatInput + dog: DogInput + snake: SnakeInput } ``` @@ -118,7 +118,7 @@ mutation { location: "Portland, OR" animals: [ {name: "Buster", age: 3, livesLeft: 7}, - {name: "Ripple", age: 2, __typename: "Dog"}, + {name: "Ripple", age: 2, __typename: "DogInput"}, ] ) } From 2e37685208df7eb2092b6370cbeeb1b3c9cd750b Mon Sep 17 00:00:00 2001 From: Vince Foley Date: Wed, 18 Sep 2019 09:36:12 -0700 Subject: [PATCH 6/7] note nested inputs --- rfcs/InputUnion.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/InputUnion.md b/rfcs/InputUnion.md index 30609909d..1c0549cf5 100644 --- a/rfcs/InputUnion.md +++ b/rfcs/InputUnion.md @@ -106,7 +106,7 @@ In addition, relying on this layer of abstraction means that this domain must be } ``` -Another common approach is to provide a unique mutation for every type. A schema employing this technique might have `logCatDropOff`, `logDogDropOff` and `logSnakeDropOff` mutations. This removes the potential for modeling non-sensical situations, but it explodes the number of mutations in a schema, making the schema less accessible. +Another common approach is to provide a unique mutation for every type. A schema employing this technique might have `logCatDropOff`, `logDogDropOff` and `logSnakeDropOff` mutations. This removes the potential for modeling non-sensical situations, but it explodes the number of mutations in a schema, making the schema less accessible. If the type is nested inside other inputs, this approach simply isn't feasable. These workarounds only get worse at scale. Real world GraphQL schemas can have dozens if not hundreds of possible types for a single `Interface` or `Union`. From 3532cc6edc1cde132ad35e4ff9b1e9ed105a03f0 Mon Sep 17 00:00:00 2001 From: Vince Foley Date: Thu, 19 Sep 2019 14:17:18 -0700 Subject: [PATCH 7/7] clarify final mutation sketch --- rfcs/InputUnion.md | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/rfcs/InputUnion.md b/rfcs/InputUnion.md index 1c0549cf5..df6902fdb 100644 --- a/rfcs/InputUnion.md +++ b/rfcs/InputUnion.md @@ -116,25 +116,22 @@ The goal of the **Input Union** is to bring a polymorphic type to Inputs. This w mutation { logAnimalDropOff( location: "Portland, OR" + + # Problem: we need to determine the type of each Animal animals: [ + # This is meant to be a CatInput {name: "Buster", age: 3, livesLeft: 7}, - {name: "Ripple", age: 2, __typename: "DogInput"}, + + # This is meant to be a DogInput + {name: "Ripple", age: 2}, ] ) } ``` -```graphql -inputunion AnimalDropOff = Dog | Cat | Snake - -type Mutation { - logAnimalDropOff(location: String, animals: [AnimalDropOff]): Boolean -} -``` - -In this mutation, we encounter the main challenge of the **Input Union** - we need to determine the correct type of the data submitted. In a query, we have the `... on Cat` fragment which explicitly identifies the concrete type of the interface or union. +In this mutation, we encounter the main challenge of the **Input Union** - we need to determine the correct type of the data submitted. -A wide variety of solutions have been explored by the community, and they are outlined in detail in this document under [Possible Solutions](#Possible-Solutions). In the example above we employed a `__typename` field to indicate the type of animal, but this is only one possible solution. +A wide variety of solutions have been explored by the community, and they are outlined in detail in this document under [Possible Solutions](#Possible-Solutions). ## Use Cases