From 719c6991d4d5d2fe21c466bc318922c62eedc804 Mon Sep 17 00:00:00 2001 From: Vince Foley <39946+binaryseed@users.noreply.github.com> Date: Tue, 14 May 2019 21:50:24 -0700 Subject: [PATCH 1/4] Start Input Union RFC document --- rfcs/InputUnion.md | 119 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 rfcs/InputUnion.md diff --git a/rfcs/InputUnion.md b/rfcs/InputUnion.md new file mode 100644 index 000000000..db53dabda --- /dev/null +++ b/rfcs/InputUnion.md @@ -0,0 +1,119 @@ +RFC: GraphQL Input Union +---------- + +## Possible Solutions + +### Value-based Discriminator field + +These options rely the _value_ of a particular input field to determine the concrete type. + +#### Single `__typename` field; value is the `type` + +```graphql +input SummaryMetric { + min: Integer + max: Integer +} +input CounterMetric { + count: Integer +} +input HistogramMetric { + percentiles: [Integer] +} + +inputUnion MetricInputUnion = SummaryMetric | CounterMetric | HistogramMetric + +{__typename: "SummaryMetric", min: 123, max: 456} +{__typename: "CounterMetric", count: 789} +{__typename: "HistogramMetric", percentiles: [12, 34, 56, 78, 90]} +``` + +#### Single user-chosen field; value is the `type` + +```graphql +input SummaryMetric { + metricType: + min: Integer + max: Integer +} +input CounterMetric { + metricType: + count: Integer +} +input HistogramMetric { + metricType: + percentiles: [Integer] +} + +inputUnion MetricInputUnion = SummaryMetric | CounterMetric | HistogramMetric + +{metricType: "SummaryMetric", min: 123, max: 456} +{metricType: "CounterMetric", count: 789} +{metricType: "HistogramMetric", percentiles: [12, 34, 56, 78, 90]} +``` + +#### Single user-chosen field; value is a literal + +```graphql +enum MetricType { + SUMMARY + COUNTER + HISTOGRAM +} +input SummaryMetric { + metricType: MetricType::SUMMARY + min: Integer + max: Integer +} +input CounterMetric { + metricType: MetricType::COUNTER + count: Integer +} +input HistogramMetric { + metricType: MetricType::HISTOGRAM + percentiles: [Integer] +} + +inputUnion MetricInputUnion = SummaryMetric | CounterMetric | HistogramMetric + +{metricType: SUMMARY, min: 123, max: 456} +{metricType: COUNTER, count: 789} +{metricType: HISTOGRAM, percentiles: [12, 34, 56, 78, 90]} +``` + +### Structural Discrimination + +These options rely on the _structure_ of the input to determine the concrete type. + +#### Unique structure + +Schema Rule: Each type in the union must have a unique set of required fields + +```graphql +input SummaryMetric { + name: String! + min: Float! + max: Float! + count: Integer +} +input CounterMetric { + name: String! + count: Integer! +} +input HistogramMetric { + name: String! + percentiles: [Integer]! + width: Integer +} + +inputUnion MetricInputUnion = SummaryMetric | CounterMetric | HistogramMetric + +{name: "my.metric", min: 123.4, max: 456.7, count: 89} +{name: "my.metric", count: 789} +{name: "my.metric", percentiles: [12, 34, 56, 78, 90]} +``` + +Problems: + +* Optional fields could prevent determining a unique type + From 5b94058bb448805cd7948ed20ab3af89505e8a51 Mon Sep 17 00:00:00 2001 From: Vince Foley Date: Thu, 16 May 2019 20:48:16 -0700 Subject: [PATCH 2/4] adopt common example types --- rfcs/InputUnion.md | 195 ++++++++++++++++++++++++++++++--------------- 1 file changed, 129 insertions(+), 66 deletions(-) diff --git a/rfcs/InputUnion.md b/rfcs/InputUnion.md index db53dabda..9ed01e0fa 100644 --- a/rfcs/InputUnion.md +++ b/rfcs/InputUnion.md @@ -3,117 +3,180 @@ RFC: GraphQL Input Union ## Possible Solutions -### Value-based Discriminator field +Categories: -These options rely the _value_ of a particular input field to determine the concrete type. +* Value-based discriminator field +* Structural discrimination + +### Value-based discriminator field + +These options rely the _value_ of a specific input field to express the concrete type. #### Single `__typename` field; value is the `type` ```graphql -input SummaryMetric { - min: Integer - max: Integer +input AddPostInput { + title: String! + body: String! } -input CounterMetric { - count: Integer -} -input HistogramMetric { - percentiles: [Integer] +input AddImageInput { + title: String! + photo: String! + caption: String } -inputUnion MetricInputUnion = SummaryMetric | CounterMetric | HistogramMetric +inputUnion AddMediaBlockInput = AddPostInput | AddImageInput + +type Mutation { + addContent(content: AddMediaBlockInput!): Content +} -{__typename: "SummaryMetric", min: 123, max: 456} -{__typename: "CounterMetric", count: 789} -{__typename: "HistogramMetric", percentiles: [12, 34, 56, 78, 90]} +# Variables: +{ + content: { + "__typename": "AddPostInput", + title: "Title", + body: "body..." + } +} ``` #### Single user-chosen field; value is the `type` ```graphql -input SummaryMetric { - metricType: - min: Integer - max: Integer +input AddPostInput { + kind: + title: String! + body: String! } -input CounterMetric { - metricType: - count: Integer -} -input HistogramMetric { - metricType: - percentiles: [Integer] +input AddImageInput { + kind: + title: String! + photo: String! + caption: String } -inputUnion MetricInputUnion = SummaryMetric | CounterMetric | HistogramMetric +inputUnion AddMediaBlockInput = AddPostInput | AddImageInput -{metricType: "SummaryMetric", min: 123, max: 456} -{metricType: "CounterMetric", count: 789} -{metricType: "HistogramMetric", percentiles: [12, 34, 56, 78, 90]} +type Mutation { + addContent(content: AddMediaBlockInput!): Content +} + +# Variables: +{ + content: { + kind: "AddPostInput", + title: "Title", + body: "body..." + } +} ``` #### Single user-chosen field; value is a literal ```graphql -enum MetricType { - SUMMARY - COUNTER - HISTOGRAM +enum MediaType { + POST + IMAGE } -input SummaryMetric { - metricType: MetricType::SUMMARY - min: Integer - max: Integer +input AddPostInput { + kind: MediaType::POST + title: String! + body: String! } -input CounterMetric { - metricType: MetricType::COUNTER - count: Integer -} -input HistogramMetric { - metricType: MetricType::HISTOGRAM - percentiles: [Integer] +input AddImageInput { + kind: MediaType::IMAGE + title: String! + photo: String! + caption: String } -inputUnion MetricInputUnion = SummaryMetric | CounterMetric | HistogramMetric +inputUnion AddMediaBlockInput = AddPostInput | AddImageInput -{metricType: SUMMARY, min: 123, max: 456} -{metricType: COUNTER, count: 789} -{metricType: HISTOGRAM, percentiles: [12, 34, 56, 78, 90]} +type Mutation { + addContent(content: AddMediaBlockInput!): Content +} + +# Variables: +{ + content: { + kind: "POST", + title: "Title", + body: "body..." + } +} ``` -### Structural Discrimination +### Structural discrimination These options rely on the _structure_ of the input to determine the concrete type. #### Unique structure -Schema Rule: Each type in the union must have a unique set of required fields +Schema Rule: Each type in the union must have a unique set of required field names + +```graphql +input AddPostInput { + title: String! + body: String! +} +input AddImageInput { + photo: String! + caption: String +} + +inputUnion AddMediaBlockInput = AddPostInput | AddImageInput + +type Mutation { + addContent(content: AddMediaBlockInput!): Content +} + +# Variables: +{ + content: { + title: "Title", + body: "body..." + } +} +``` + +An invalid schema: ```graphql -input SummaryMetric { - name: String! - min: Float! - max: Float! - count: Integer +input AddPostInput { + title: String! + body: String! } -input CounterMetric { - name: String! - count: Integer! +input AddDatedPostInput { + title: String! + body: String! + date: Int } -input HistogramMetric { - name: String! - percentiles: [Integer]! - width: Integer +input AddImageInput { + photo: String! + caption: String } -inputUnion MetricInputUnion = SummaryMetric | CounterMetric | HistogramMetric +inputUnion AddMediaBlockInput = AddPostInput | AddDatedPostInput | AddImageInput -{name: "my.metric", min: 123.4, max: 456.7, count: 89} -{name: "my.metric", count: 789} -{name: "my.metric", percentiles: [12, 34, 56, 78, 90]} +type Mutation { + addContent(content: AddMediaBlockInput!): Content +} ``` Problems: * Optional fields could prevent determining a unique type +```graphql +input AddPostInput { + title: String! + body: String! + date: Int +} +input AddDatedPostInput { + title: String! + body: String! + date: Int! +} +``` From 5d04f83d14494bb1bc74049e6fee96853219efd5 Mon Sep 17 00:00:00 2001 From: Vince Foley Date: Thu, 16 May 2019 21:43:59 -0700 Subject: [PATCH 3/4] add order based solution; clean-up formatting --- rfcs/InputUnion.md | 103 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 4 deletions(-) diff --git a/rfcs/InputUnion.md b/rfcs/InputUnion.md index 9ed01e0fa..482c51c83 100644 --- a/rfcs/InputUnion.md +++ b/rfcs/InputUnion.md @@ -1,6 +1,12 @@ RFC: GraphQL Input Union ---------- +The addition of an Input Union type has been discussed in the GraphQL community for many years now. The value of this feature has largely been agreed upon, but the implementation has not. + +This document attempts to bring together all the various solutions that have been discussed with the goal of reaching a shared understanding of the problem space. + +From that shared understanding, this document can then evolve into a proposed solution. + ## Possible Solutions Categories: @@ -10,10 +16,12 @@ Categories: ### Value-based discriminator field -These options rely the _value_ of a specific input field to express the concrete type. +These options rely the **value** of a specific input field to express the concrete type. #### Single `__typename` field; value is the `type` +This solution was discussed in https://github.com/graphql/graphql-spec/pull/395 + ```graphql input AddPostInput { title: String! @@ -41,6 +49,10 @@ type Mutation { } ``` +##### Variations: + +* A `default` annotation may be provided, for which specifying the `__typename` is not required. This enables a field migration from an `Input` to an `Input Union` + #### Single user-chosen field; value is the `type` ```graphql @@ -72,8 +84,14 @@ type Mutation { } ``` +##### Problems: + +* The discriminator field is non-sensical if the input is used _outside_ of an input union. + #### Single user-chosen field; value is a literal +This solution is derrived from one discussed in https://github.com/graphql/graphql-spec/issues/488 + ```graphql enum MediaType { POST @@ -107,11 +125,74 @@ type Mutation { } ``` +##### Variations: + +* Literal strings used instead of an `enum` + +```graphql +input AddPostInput { + kind: 'post' + title: String! + body: String! +} +input AddImageInput { + kind: 'image' + title: String! + photo: String! + caption: String +} +``` + +##### Problems: + +* The discriminator field is redundant if the input is used _outside_ of an input union. + ### Structural discrimination -These options rely on the _structure_ of the input to determine the concrete type. +These options rely on the **structure** of the input to determine the concrete type. + +#### Order based type matching -#### Unique structure +The concrete type is the first type in the input union definition that matches. + +```graphql +input AddPostInput { + title: String! + publishedAt: Int + body: String +} +input AddImageInput { + title: String! + publishedAt: Int + photo: String + caption: String +} + +inputUnion AddMediaBlockInput = AddPostInput | AddImageInput + +type Mutation { + addContent(content: AddMediaBlockInput!): Content +} + +# Variables: +{ + content: { + title: "Title", + date: 1558066429 + # AddPostInput + } +} +{ + content: { + title: "Title", + date: 1558066429 + photo: "photo.png" + # AddImageInput + } +} +``` + +#### Structural uniqueness Schema Rule: Each type in the union must have a unique set of required field names @@ -136,6 +217,7 @@ type Mutation { content: { title: "Title", body: "body..." + # AddPostInput } } ``` @@ -164,7 +246,7 @@ type Mutation { } ``` -Problems: +##### Problems: * Optional fields could prevent determining a unique type @@ -180,3 +262,16 @@ input AddDatedPostInput { date: Int! } ``` + +Workaround? : Each type's set of required fields must be uniquely identifying + + - A type's set of required field names must not match the set of another type's required field names + - A type's set of required field names must not overlap with the set of another type's required or optional field names + +Workaround? : Each type must have at least one unique required field + + - A type must contain one required field that is not a field in any other type + +##### Variations: + +* Consider the field _type_ along with the field _name_ when determining uniqueness. From 843851bfbbb2ee6f7da65eb40bcd3df0afe8a94c Mon Sep 17 00:00:00 2001 From: Vince Foley Date: Tue, 21 May 2019 21:21:20 -0700 Subject: [PATCH 4/4] add tagged union --- rfcs/InputUnion.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/rfcs/InputUnion.md b/rfcs/InputUnion.md index 482c51c83..377eadf45 100644 --- a/rfcs/InputUnion.md +++ b/rfcs/InputUnion.md @@ -275,3 +275,40 @@ Workaround? : Each type must have at least one unique required field ##### Variations: * Consider the field _type_ along with the field _name_ when determining uniqueness. + +#### One Of (Tagged Union) + +This solution was presented in https://github.com/graphql/graphql-spec/pull/395#issuecomment-361373097 + +The type is determined by using an intermediate input type that maps field name to type. + +A directive has also been discussed to specify that only one of the fields may be selected. See https://github.com/graphql/graphql-spec/pull/586. + +```graphql +input AddPostInput { + title: String! + body: String! +} +input AddImageInput { + photo: String! + caption: String +} +input AddMediaBlockInput @oneOf { + post: AddPostInput + image: AddImageInput +} + +type Mutation { + addContent(content: AddMediaBlockInput!): Content +} + +# Variables: +{ + content: { + post: { + title: "Title", + body: "body..." + } + } +} +```