Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] GraphQL Input Union type #488

Closed
frikille opened this issue Aug 2, 2018 · 98 comments
Closed

[RFC] GraphQL Input Union type #488

frikille opened this issue Aug 2, 2018 · 98 comments
Labels
💭 Strawman (RFC 0) RFC Stage 0 (See CONTRIBUTING.md)

Comments

@frikille
Copy link

frikille commented Aug 2, 2018

[RFC] GraphQL Input Union type

Background

There have been many threads on different issues and pull requests regarding this feature. The main discussion started with the graphql/graphql-js#207 on 17/10/2015.

Currently, GraphQL input types can only have one single definition, and the more adoption GraphQL gained in the past years it has become clear that this is a serious limitation in some use cases. Although there have been multiple proposals in the past, this is still an unresolved issue.

With this RFC document, I would like to collect and summarise the previous discussions at a common place and proposals and propose a new alternative solution.

To have a better understanding of the required changes there is a reference implementation of this proposal, but that I will keep up to date based on future feeback on this proposal.

The following list shows what proposals have been put forward so far including a short summary of pros and cons added by @treybrisbane in this comment

  • __inputname field by @tgriesser

    RFC document: RFC: inputUnion type #395

    Reference implementation: RFC: inputUnion type graphql-js#1196

    This proposal was the first official RFC which has been discussed at the last GraphQL Working Group meeting.
    This proposal in this current form has been rejected by the WG because of the __inputname semantics. However, everyone agrees that alternative proposals should be explored.

    • Pros:
      • Expresses the design intent within the schema
      • Supports unions of types with overlapping fields
      • Removes the costs of the tagged union pattern (both to the schema and field resolution)
      • Addresses the feature asymmetry of unions within the type system
    • Cons:
      • Adds complexity to the language in the form of input union-specific syntax
      • Adds complexity to the language in the form of additional validation (around __inputtype, etc)
      • Adds complexity to the request protocol in the form of a (input union-specific) constraint
  • Tagged union by @leebyron and @IvanGoncharov

    Original comment

    input MediaBlock = { post: PostInput! } | { image: ImageInput! }
    • Pros:
      • Expresses the design intent within the schema
      • Removes the costs of the tagged union pattern (both to the schema and field resolution)
      • Does not require changes to the request protocol
    • Cons:
      • No support for unions of types with overlapping fields
      • Introduces inconsistencies between the syntax for output unions and the syntax for input unions
      • Adds complexity to the language in the form of input union-specific syntax
      • Adds complexity to the language in the form of additional validation (around enforcing the stipulations on overlapping fields, nullability, etc)
      • Does not fully address the feature asymmetry of unions within the type system
  • Directive

    Original comment

    input UpdateAccountInput @inputUnion {
      UpdateAccountBasicInput: UpdateAccountBasicInput
      UpdateAccountContactInput: UpdateAccountContactInput
      UpdateAccountFromAdminInput: UpdateAccountFromAdminInput
      UpdateAccountPreferencesInput: UpdateAccountPreferencesInput
    }
    • Pros:
      • Requires no language or request prototcol changes beyond a new directive
      • Supports unions of types with overlapping fields
    • Cons:
      • Adds complexity to the language in the form of a new directive
      • Does not express the design intent within the schema (the presence of a directive completely changes the meaning of a type definition which would otherwise be used to describe a simple object type)
      • Does not remove the costs of the tagged union pattern
      • Does not address the feature asymmetry of unions within the type system

Proposed solution

Based on the previous discussions I would like to propose an alternative solution by using a disjoint (or discriminated) union type.

Defining a disjoint union has two requirements:

  1. Have a common literal type field - called the discriminant
  2. A type alias that takes the union of those types - the input union

With that our GraphQL schema definition would be the following (Full schema definition)

literal ImageInputKind
literal PostInputKind

input AddPostInput {
  kind: PostInputKind!
  title: String!
  body: String!
}

input AddImageInput  {
  kind: ImageInputKind!
  photo: String!
  caption: String
}

inputUnion AddMediaBlock = AddPostInput | AddImageInput

input EditPostInput {
  inputTypeName: PostInputKind!
  title: String
  body: String
}

input EditImageInput {
  inputTypeName: ImageInputKind!
  photo: String
  caption: String
}

inputUnion EditMediaBlock = EditPostInput | EditImageInput

type Mutation {
  createMedia(content: [AddMediaBlock]!): Media   
  editMedia(content: [EditMediaBlock]!): Media
}

And a mutation query would be the following:

mutation {
  createMedia(content: [{
    kind: PostInputKind
    title: "My Post"
    body: "Lorem ipsum ..."
  }, {
    kind: ImageInputKind
    photo: "http://www.tanews.org.tw/sites/default/files/imce/admin/user2027/6491213611_c4fc290a33_z.jpg"
    caption: "Dog"
  }]) {
    content {
      ... on PostBlock {
        title
        body
      }
      ... on ImageBlock {
        photo
        caption
      }
    }
  }
}

or with variables

mutation CreateMediaMutation($content: [AddMediaBlock]!) {
  createMedia(content: $content) {
    id
    content {
      ... on PostBlock {
        title
        body
      }
      ... on ImageBlock {
        photo
        caption
      }
    }
  }
}
{
  "content": [{
    "kind": "PostInputKind",
    "title": "My Post",
    "body": "Lorem ipsum ...",
  }, {
    "kind": "ImageInputKind",
    "photo": "http://www.tanews.org.tw/sites/default/files/imce/admin/user2027/6491213611_c4fc290a33_z.jpg",
    "caption": "Dog"
  }]
}

New types

Literal type

The GraphQLLiteralType is an exact value type. This new type enables the definition a discriminant field for input types that are part of an input union type.

Input union type

The GraphQLInputUnionType represent an object that could be one of a list of GraphQL Input Object types.

Input Coercion

The input union type needs a way of determining which input object a given input value correspond to. Based on the discriminant field it is possible to have an internal function that can resolve the correct GraphQL Input Object type. Once it has been done, the input coercion of the input union is the same as the input coercion of the input object.

Type Validation

Input union types have a potential to be invalid if incorrectly defined:

  • An input union type must include one or more unique input objects
  • Every included input object type must have:
    • One common field
    • This common field must be a unique Literal type for every Input Object type

Using String or Enum types instead of Literal types

While I think it would be better to add support for a Literal type, I can see that this type would only be useful for Input unions; therefore, it might be unnecessary. However, it would be possible to use String or Enum types for the discriminant field, but in this case, a resolveType function must be implemented by the user. This would also remove one of the type validations required by the input type (2.2 - The common field must be a unique Literal type for every Input Object type).

Final thoughts

I believe that the above proposal addresses the different issues that have been raised against earlier proposals. It introduces additional syntax and complexity but uses a concept (disjoint union) that is widely used in programming languages and type systems. And as a consequence I think that the pros of this proposal outweigh the cons.

  • Pros

    • Expresses the design intent within the schema
    • The discriminant field name is configurable
    • Supports unions of types with overlapping fields
    • Addresses the feature asymmetry of unions within the type system
    • Does not require changes to the request protocol
  • Cons

    • Adds complexity to the language in the form of literal-specific syntax (might not need)
    • Adds complexity to the language in the form of input union-specific syntax
    • Adds complexity to the language in the form of additional validations (input union, discriminant field, input union resolving (might not need))

However, I think there are many questions that needs some more discussions, until a final proposal can be agreed on - especially around the new literal type, and if it is needed at all.

@IvanGoncharov
Copy link
Member

Currently, GraphQL input types can only have one single definition, and the more adoption GraphQL gained in the past years it has become clear that this is a serious limitation in some use cases.

This is the main point of this discussion, instead of proposing competing solutions we should focus on defining a set of problems that we want to address. From previous discussions, it looks like the main use case is "super" mutations. If this is also the case for you RFC should address why you can't do separate createMediaPost or createMediaImage. Do you have any other use real-life use cases that you want to address with this feature?

So this discussion should be driven by real-life user cases and the proposed solution should be presented only in the context of solving this use cases.
IMHO, without a defined set of the use cases, this discussion looks like a beauty contest for SDL syntax and JSON input.

Does not address the feature asymmetry of unions within the type system

Feature asymmetry between input and output types is not an issue since they are fundamentally different in API case.

Output types represent entities from the business domain (e.g. Post, Image, etc.) and because of that, they are usually reused all around the schema.

Input types represent set of data required for a particular action or query (e.g. AddPostInput, EditPostInput) and thus in the majority of cases, input types have very limited reusability.

IMHO, that's why it makes sense to sacrifice some simplicity to allow better expressing your domain entities and facilitate reusability by allowing interfaces and unions on output types.

As for input types instead of the feature-reach type system, we need to provide set of validation constraints, e.g. based on your example this type will satisfy:

type AddMediaBlock {
  kind: PostInputKind!
  title: String
  body: String
  photo: String
  caption: String
}

It's working and fully solve the present problem there are the only two problems:

  • documentation - you need to describe valid field combination in description
  • validation - you need to write validation manually

So instead of creating two additional types and complicating type system with inputUnion we can just solve the above problems by describing required validation constraints: type AddMediaBlock = { ... } | { ... }.

Using String or Enum types instead of Literal types

IMHO, literal is just polute namespace and force to use PostInputKind istead of Post.
We shouldn't mix client data with typenames from typesystem in any shape or form especially since it was one of main arguments against __inputname.

So it should be either in place defined strings or enum values.

@frikille
Copy link
Author

frikille commented Aug 2, 2018

From previous discussions, it looks like the main use case is "super" mutations.
Do you have any other use real-life use cases that you want to address with this feature?

Yes. We are building a headless content management platform: users can define their own content types, fields and valiations. They get a GraphQL API that they can use to read and write data. And we want to support a new feature whereby a content type field can have multiple types. So an output type would be something like this:

type PageDocument {
  id: ID
  title: String
  slug: String
  author: Author
  sections: [Section]
}

type Section {
  title: String
  blocks: [SectionBlock]
}

union SectionBlock = ImageGallery | HighlightedImage | ArticleList | RichText

As you can see, this doesn't requires a top level "super" mutation. I agree with you on that a super mutation can and should be just separate mutations. But in our case it is not not possible since the input field that can have multiple types is deeply nested.

An example schema for the input types:

mutation createPageDocument(document: PageDocumentInput!): PageDocument

input PageDocumentInput {
  title: String!
  slug: String!
  author: ID!
  sections: [SectionInput]
}

input SectionInput = {
  title: String!
  blocks: [SectionBlockInput]
}

enum SectionBlockInputEnum = {
  ImageGallery
  HighlightedImage
  Articles
  RichText
}

input ImageGalleryInput = {
  kind: SectionBlockInputTypeEnum!
  title: String!
  description: String!
  imageUrls: [String!]!
}

input HighlightedImageInput {
  kind: SectionBlockInputTypeEnum!
  url: String!
  caption: String!
}

input ArticaleListInput {
  kind: SectionBlockInputTypeEnum!
  articleIds: [ID!]!
}

input RichTextInput {
  kind: SectionBlockInputTypeEnum!
  html: String
  markdown: String
}

I deliberately left out the definition of the SectionBlockInput input type. Based on my proposal it would be this:

inputUnion SectionBlockInput = ImageGalleryInput | HighlightedImageInput | ArticleListInput | RichTextInput

I would skip the "super" SectionBlockInput type definition, that includes every field from the four input types, because as you mentioned that have two problems:

  • documentation: We must be able to generate the best documentation to our users since they can use the API for writing data in to their project
  • validation: this is the bigger issue. We started with GraphQL mostly because of the type system and query validation it provides. And while we can write a validation engine that solves this problem, we feel that it's just another validation should live in GraphQL.

With your required validation constraint suggestion I suppose it would be something like this:

input SectionBlockInput = {  
  kind: SectionBlockInputTypeEnum!
  title: String!
  description: String!
  imageUrls: [String!]!
} | {
  kind: SectionBlockInputTypeEnum!
  url: String!
  caption: String!
} | {
  kind: SectionBlockInputTypeEnum!
  articleIds: [ID!]!
} | {
  kind: SectionBlockInputTypeEnum!
  html: String
  markdown: String
}

I think these two examples are pretty close to each other and would achieve the same result. The main question is that it would be better doing this:

  • with a new type (inputType)
  • or by new validation constraint

IMHO, literal is just polute namespace and force to use PostInputKind istead of Post.
We shouldn't mix client data with typenames from typesystem in any shape or form especially since it was one of main arguments against __inputname.

I accept this.It had a lot of advantages using it when I implemented the input type resolver. I would be happy to drop this one completely as it already felt a bit unnecessary.

@IvanGoncharov
Copy link
Member

@frikille Thank you for sharing your use case it helped me a lot with understanding your particular problem.
Is it the only type of scenarios where you need Input Union types?

@frikille
Copy link
Author

frikille commented Aug 3, 2018

There are some other places where it would be useful, but those are top level mutations (creating a document) which can be solved other ways. However the input union support would help with that use case as well. Our main problem is that we have completely different schemas for every user project, and it would be better if we were able to use a common mutation for creating a document instead of dynamically changing the mutation name. But I understand that it can be solved without input unions, and we've been using that solution for a long time so for that it is not necessary. However, we don't see how the use case I described in my previous comment can be solved without input unions.

@jturkel
Copy link
Contributor

jturkel commented Aug 3, 2018

Creating/updating an entity (PageDocument in the example above) with a heterogeneous collection of nested value objects (different types of Section in the example above) is a great use case for input unions. If you're looking for more use cases @xuorig describes batching multiple mutating operations (each of which may have very different structure) into a single atomic mutation in this blog post. The post describes a project board like Trello where a user can add/move/delete/update cards in a single atomic operation. The corresponding SDL looks something like this with input unions:

type Mutation {
  updateBoard(input: UpdateBoardInput!): UpdateBoardPayload!
}

type UpdateBoardInput {
  operations: [Operation!]!
}

union Operation = AddOperation | UpdateOperation |  MoveOperation | DeleteOperation

input AddOperation {
  id: ID!
  body: String!
}

input UpdateOperation {
  id: ID!
  body: String!
}

input MoveOperation {
  id: ID!
  newPos: Int!
}

input DeleteOperation {
  id: ID!
  body: String!
}

@OlegIlyenko also described similar ideas for batching multiple operations into a single atomic mutation in the context of CQRS and commerce in this blog post.

@IvanGoncharov
Copy link
Member

@frikille @jturkel Thanks for more examples I will try to study them this weekend.
But from the first glance, it looks like all examples are centered around mutations.

Next major question is how proposed input unions affect code generation especially for strong type languages, e.g. Java. I suspect that such languages don't support unions based on field value as the discriminator. It especially important for mobile clients.
Can someone check how it will affect apollo-ios and apollo-android?

@mjmahone Maybe you know, can something like this affect Facebook codegen for iOS and Android?

@jturkel
Copy link
Contributor

jturkel commented Aug 4, 2018

@IvanGoncharov - We have examples in the product information management space where input unions would be useful as field arguments. On such example is retrieving a filtered list of products:

input StringEqualityFilter {
  propertyId: String!
  value: String!
}

input NumericRangeFilter {
  propertyId: String!
  minValue: Int!
  maxValue: Int!
}

input HasNoValueFilter {
  propertyId: String!
}

inputunion Filter = StringEqualityFilter | NumericRangeFilter | HasNoValueFilter

type Query {
  products(filters: [Filter!]): ProductCursor
}

Given this schema customers can retrieve a list of products that meet the following conditions:

  • The 'Brand' property is equal to 'Apple'
  • The 'Category' property is equal 'Laptops'
  • The 'Price' property is between 1000 and 2000
  • The 'Description' property has no value

Note our customers can create their own "schema" for modelling product information so our GraphQL schema can't have hardcoded knowledge of things like brands and categories thus the more general propertyId in this example.

@treybrisbane
Copy link

@IvanGoncharov I agree that we should try to keep focused on practical use cases for input unions. A really big one is pretty much any mutation that leverages recursive input types.

I'm looking at implementing a GraphQL API for a pattern-matching engine that looks something like this:

type Query {
  patternMatcher: PatternMatcher!
}

type PatternMatcher {
  cases: [Case!]!
}

type Case {
  condition: Condition
  action: Action!
}

union Condition = AndCondition | OrCondition | NotCondition | PropertyEqualsCondition # ...etc

type AndCondition {
  leftCondition: Condition!
  rightCondition: Condition!
}

type OrCondition {
  leftCondition: Condition!
  rightCondition: Condition!
}

type NotCondition {
  condition: Condition!
}

type PropertyEqualsCondition {
  property: String!
  value: String!
}

# ...etc

union Action = # ...you get the idea

Naturally, I need to be able to create new Cases. Ideally, this would look something like this:

type Mutation {
  createCase(input: CreateCaseInput!): CreateCasePayload!
}

input CreateCaseInput {
  caseSpecification: CaseSpecification!
}

type CreateCasePayload {
  case: Case!
}

input CaseSpecification {
  condition: ConditionSpecification
  action: ActionSpecification!
}

inputunion ConditionSpecification = AndConditionSpecification | OrConditionSpecification | NotConditionSpecification | PropertyEqualsConditionSpecification # ...etc

input AndConditionSpecification {
  leftCondition: ConditionSpecification!
  rightCondition: ConditionSpecification!
}

input OrConditionSpecification {
  leftCondition: ConditionSpecification!
  rightCondition: ConditionSpecification!
}

input NotConditionSpecification {
  condition: ConditionSpecification!
}

input PropertyEqualsConditionSpecification {
  property: String!
  value: String!
}

# ...etc

inputunion ActionSpecification = # ...again, you get the idea

Describing this properly without input unions is impossible. The current workaround I'm using is the "tagged-union pattern", however in my experience, use of that pattern is continually met with confusion and distaste due to its verbosity and lack of clear intent.

Note that your suggestion of input Condition = { ... } | { ... } really doesn't scale here. The ability to name the branches of the input union is pretty much necessary to maintain readability in large unions. Plus, I don't see any advantages in forcing input union branches to be anonymous, especially when the same is not true for output unions.

Anyway, that's my use case. I don't really consider it a "super mutation"; it's a simple mutation that just happens to use recursive input types. :)

@treybrisbane
Copy link

@frikille Thanks for writing this up. :)

I definitely see why you went with the literal type. What was the rational behind going with a separate literal Foo declaration rather than just this:

input Foo {
  kind: "Foo"!
  # ...etc
}

???

@frikille
Copy link
Author

frikille commented Aug 9, 2018

@treybrisbane We were thinking about that and we think that supporting inline strings would be a nice syntax sugar but at the end, it would be parsed to a new type, so because of clarity we added the literal type (also I kinda prefer to declare everything properly)

@IvanGoncharov I'm not that familiar with Java any more, but it is a good point, and as far as I know, it doesn't have support for discriminated unions.

@IvanGoncharov
Copy link
Member

I'm not that familiar with Java any more, but it is a good point, and as far as I know, it doesn't have support for discriminated unions.

Mobile is a pretty critical platform for GraphQL, so it would be great to know how it would be affected. So would be great if someone knowledgeable can answer my previous question:

how proposed input unions affect code generation especially for strong type languages, e.g. Java. I suspect that such languages don't support unions based on field value as the discriminator. It especially important for mobile clients.
Can someone check how it will affect apollo-ios and apollo-android?

@sorenbs
Copy link

sorenbs commented Aug 13, 2018

I am very happy that progress is being made on union input types 🎉

However, if I understand the proposal correctly, then it will not address the use cases we have in mind for the Prisma API.

Here is a spec for a new feature we will be implementing soon. I have written up how we would like to shape the API if union input types would support it. I have also described why the proposed solution with a discriminating field does not work for us: prisma/prisma1#1349

I'm curious to hear if others are seeing use cases similar to us, or if we are special for implementing a very generic API?

Let me know if more details are needed.

@dendrochronology
Copy link

@sorenbs would you mind explaining your reasoning a bit more, or perhaps provide some basic query examples, please? Perhaps I'm misunderstanding your atomic actions proposal, but it seems much broader than what's being discussed here.

I think the core issue this RFC addresses is to provide a simple, declarative way to allow for multiple input types on query and mutation fields. That's a severe weakness of GraphQL when it comes to data modeling.

A lot of my recent work has been porting data from legacy relational CMS's into modern GraphQL environments, and this RFC would solve one of the biggest headaches I've had. I'd rather see the community focus on feature-lean solutions to the biggest gaps first (i.e., things without good workarounds), and focus on narrower issues in later iterations.

@sorenbs
Copy link

sorenbs commented Aug 13, 2018

@dendrochronology - sorry for just linking to a giant feature spec, thats certainly not the best way to illustrate my point :-)

We have evolved our generic GraphQL API over the last 3 years. At 4-5 times throughout this process has it been brought up that some form of union input type could simplify the API design. Most of these cases boils down to supporting either a specific scalar type or a specific complex type:

given the types

type Complex {
  begin: DateTime
  end: DateTime
}

union scalarOrComplex = Int | Complex

type mutation {
  createTiming(duration: scalarOrComplex): Int
}

Create a timing providing duration in milliseconds or as a complex structure containing start and end DateTime:

mutation {
  simple: createTiming(duration: 120)
  complex: createTiming(duration: {begin: "2017", end: "2018"})
}

For us it is paramount that we maintain a simple API.

Did this example make it clear what we are looking to achieve?

It might also be worth noting that we have found it valuable on many occasions that A single element is treated the same as an array with a single element. This flexibility has made it easier to evolve our API in many cases, and I think we can achieve something similar for union input types if we choose a system that solves for that.

From this perspective, we would prefer the Tagged union by @leebyron and @IvanGoncharov described above. Keep in mind that I have not thought through the implications for tooling and strongly typed languages.

@dendrochronology
Copy link

dendrochronology commented Aug 14, 2018

Thanks, @sorenbs, those are great examples. I do like the simplicity, and agree that robust APIs should handle singles vs. arrays elegantly, a 'la Postel's Law.

Back to @IvanGoncharov's request for a use case-driven discussion, I think that the union type implementation needs to go a bit further than generic APIs. A lot of my work is content and UX-based, where a client application pushes/pulls lots of heterogeneous objects from a back end, and displays them to a user for viewing/interaction.

For example, think of a basic drag-and-drop CMS interface, where a user can compose multiple blocks, with some levels of nesting and validation rules (e.g., block type X can have children of types A and B, but not C, etc.). With the current state of GraphQL, you need an awful lot of client-side logic to pre/post process output, because schemas have many fields with no baked-in validation or guarantees of meeting a required contract for available data or metadata fields (e.g., all blocks should have at least a type, label, color, x and y coordinates, ordinal number, etc.). I can mockup something like that in React easily, but building a GraphQL API to power it quickly gets messy.

I suppose my key takeaway is: I find that the current GraphQL spec makes it difficult to model a lot of real-world data situations without excessive verbosity and generous additional processing. I would love to be wrong, and have someone point out what I'm missing, but that hasn't happened yet 🤔

@jturkel
Copy link
Contributor

jturkel commented Aug 14, 2018

@IvanGoncharov - The proposed input union functionality for strongly typed clients should behave much the same way as "output"
unions. For Java, which doesn't support unions, this can be modeled via an interface representing the union that each member type implements. Simplified code for earlier createMedia mutation would look something like:

public interface AddMediaBlock { }

public class AddPostInput implements AddMediaBlock { }

public class AddImageInput implements AddMediaBlock { }

public class CreateMediaMutation {
  public CreateMediaMutation(AddMediaBlock[] content) { }
}

// Sample code that instantiates the appropriate union member types
AddMediaBlock[] content = new AddMediaBlock[]{new AddPostInput(...), new AddPostInput(...), new AddImageInput(...)};
client.mutate(new CreateMediaMutation(content));

Presumably Swift uses a similar strategy with protocols (the moral equivalent of Java interfaces).

@jturkel
Copy link
Contributor

jturkel commented Sep 10, 2018

@IvanGoncharov - What's your opinion on how to effectively move input unions forward? Seems like this issue has a pretty good catalog of use cases now and it's a matter of hammering out the technical details of input unions. It would be great to get a champion from the GraphQL WG to help guide this through the spec process. Is this something you have the time and desire to take on since you've already been pretty involved? I'm happy to help out in anyway I can (and I suspect others are too) but I'd like to make sure anything I do is actually helpful :)

@IvanGoncharov
Copy link
Member

What's your opinion on how to effectively move input unions forward? Seems like this issue has a pretty good catalog of use cases now and it's a matter of hammering out the technical details of input unions.

@jturkel Agree and thanks for pinging 👍

It would be great to get a champion from the GraphQL WG

Working group don't have fixed seats and if you want to raise or discuss some issue just add it to the agenda.

It would be great to get a champion from the GraphQL WG to help guide this through the spec process. Is this something you have the time and desire to take on since you've already been pretty involved?

I can help with the spec and graphql-js PRs but before that, we need to formulate a more concrete proposal. I thought about it for last couple months but I still see a number of blind spots.

So as a next step I think we should find some points that we all agree on.
On weekends, I will try to summarize this discussion and my own thoughts in something that we could discuss.

@jturkel
Copy link
Contributor

jturkel commented Sep 13, 2018

Great to hear you're up for helping out with this @IvanGoncharov! Figuring out where folks agree/disagree sounds like a perfect next step to me. Feel free to ping me on the GraphQL Slack if there's anything I can do to help.

@IvanGoncharov
Copy link
Member

@jturkel Update: Have an urgent work so I wouldn't be able to compile something until next weekends.

@dmitrif
Copy link

dmitrif commented Oct 17, 2018

Any updates on this? We are facing a similar need, unless there's a better way one may suggest:

input LocationQueryIdOptions {
  id: Int!
}

input LocationQueryCoordsOptions {
  latitude: Int!
  longitude: Int!
}

union LocationOptions = LocationQueryIdOptions | LocationQueryCoordsOptions

type Query {
  location(options: LocationOptions): Location
}

@amccloud
Copy link

@dmitrif 👍 that's pretty much exactly what I tried before finding this issue:

input PostDescriptor {
  # ...
}

input ImageDescriptor {
  # ...
}

union ObjectDescriptor = PostDescriptor | ImageDescriptor;

type Mutation {
  Comment(objectDescriptor: ObjectDescriptor!, comment: CommentInput): CommentMutations
}

jokva added a commit to jokva/oneseismic that referenced this issue Jul 1, 2021
As of now, GraphQL does not support union input types, but there is a
proposal [1][2] for the next revision. It would be a welcome feature for
a system like oneseismic, because it could support referencing the same
structure (e.g. the slice) from representationally identical but
semantically different types. Consdier this example:

Schema:
    input Lineno { val: Int! }
    input Index  { val: Int! }
    slice(dim: Int!, idx: union Lineno | Index)

Query:
    slice(dim: 0, idx: Lineno { val: 1456 })
    slice(dim: 0, idx: Index  { val: 80 })

Both these queries would refer to the same slice. The best alternative
is really just separate queries for separate types, which in the
underlying implementation normalise their inputs before hooking into a
shared code path.

That is in essence what this patch does - it splits the slice() query
into a family of queries, with different functions taking different
reference systems. This allows users to query slices without having to
map to some inline, crossline or depth/time value, which makes
oneseismic a lot easier in many situations [3].

The validation and sanity checking is moved from build() into from_json,
so that build can expect a fully hydrated, validated, and normalised
query. Having this much model validation in parsing and nowhere else
points to a design flaw (should be dedicated functions for validation
that the parser can hook into), but that's a job for later.

The serializing code was only used for testing, and is removed as
to_json is not necessarily an inverse of from_json. It is not
functionally interesting to render a query back into json at this time.

[1] https://github.com/graphql/graphql-spec/blob/main/rfcs/InputUnion.md
[2] graphql/graphql-spec#488
[3] e.g. when you have some UTM -> index map, but not UTM -> inline
@bombillazo
Copy link

So... no input union types then?

@solussd
Copy link

solussd commented Oct 11, 2021

So... no input union types then?

It appears not. :(

@acao
Copy link
Member

acao commented Oct 11, 2021

edit: #825 OneOf is successor to this spec

@bombillazo
Copy link

Tagged type proposal is even more flexible and requires less tooling changes :) see

#488 (comment)

Thanks! I'll take a look at it :)

@FahmyChaabane
Copy link

So... no input union types then?

Did you manage to solve your issue ?

@gmac
Copy link

gmac commented Nov 30, 2021

For those seeking alternatives, please consider @oneOf#825. I have yet to encounter a situation that couldn't be solved as well or better using that construct, and it works today with a simple server-side validation.

@bogdibota
Copy link

If, like me, you need a quicker fix than @oneOf#825, here you go:

export const UploadOrString = new GraphQLScalarType({
  name: 'UploadOrString',
  description: 'Upload | String',
  serialize() {
    throw new Error('UploadOrString is an input type.');
  },
  parseValue(value) {
    if (typeof value === 'string') return value;
    else return GraphQLUpload.parseValue(value);
  },
  parseLiteral() {
    throw new Error('UploadOrString.parseLiteral not implemented');
  },
});

an in .gql:

scalar UploadOrString

input ProductInput {
  # for new images: the File object
  # for exiting images: the Id of the image
  # for removing images: skip from the list
  images: [UploadOrString!]!

  # ...
}

@brilovichgal
Copy link

Hi,
I cant use inputUnion, it is writing to me that "Syntax Error: Unexpected Name "inputUnion"" what should i do?

@gmac
Copy link

gmac commented Mar 1, 2022

@brilovichgal InputUnion was only a proposed spec, it’s not actually valid GraphQL. Read up this thread for other ideas.

@brilovichgal
Copy link

@brilovichgal InputUnion was only a proposed spec, it’s not actually valid GraphQL. Read up this thread for other ideas.

So there is no real solution for it?
All the suggestions don’t work..

@gmac
Copy link

gmac commented Mar 1, 2022

@brilovichgal dunno… IMO, @oneOf works considerably better than the idea of input union ever could. It circumvents the type identity issues inherent with the input union concept (multiple possible inputs with similar fields… what type is the input?), and it allows for mixed input types (submit objects, scalars, or lists); it’s also more expressive, and uses plain GraphQL that works today. Just add a simple server-side validation on a normal input object and you’re up and running. Best of luck.

@Vinccool96
Copy link

I extremely object with the decision to use @oneOf. My use-case has me wanting to accept between { recurrenceType: RecurrenceType! }. { recurrenceType: RecurrenceType!, endDate: Date! }, and { recurrenceType: RecurrenceType!, ocurrences: Int! }. Using @oneOf just isn't possible.

@benjie
Copy link
Member

benjie commented Jan 5, 2023

input RecurrenceOption @oneOf {
  typeOnly: RecurrenceType
  typeAndEndDate: RecurrenceTypeAndEndDate
  typeAndOccurrences: RecurrenceTypeAndOccurrences
}
input RecurrenceTypeAndEndDate {
  recurrenceType: RecurrenceType!
  endDate: Date!
}
input RecurrenceTypeAndOccurrences {
  recurrenceType: RecurrenceType!
  ocurrences: Int!
}

@gmac
Copy link

gmac commented Jan 6, 2023

@Vinccool96 your specific circumstance is only suited to input union potential because all of your input types happen to have a unique composition of required fields. With optional fields in play, the backend cannot reliably disambiguate input type identities, which is where input union falls apart. The oneOf pattern is a pretty robust solution, and also solves the problem of allowing mixed input across scalars, lists, and input objects.

@benjie
Copy link
Member

benjie commented Jan 11, 2023

Since all types have the recurrenceType: RecurrenceType!, an alternative solution for @Vinccool96's use case could be:

input RecurrenceOption {
  recurrenceType: RecurrenceType!
  extra: RecurrenceExtra            # Note that this is nullable, thus optional
}

input RecurrenceExtra @oneOf {
  endDate: Date
  occurrences: Int
}

Using @oneOf just isn't possible.

Since this statement doesn't seem to hold, please could you clarify what your issue with the solutions above are, or what you think the solution should be?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💭 Strawman (RFC 0) RFC Stage 0 (See CONTRIBUTING.md)
Projects
None yet
Development

No branches or pull requests