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

GraphQLUnionType as input type #207

Closed
xpepermint opened this issue Oct 17, 2015 · 46 comments
Closed

GraphQLUnionType as input type #207

xpepermint opened this issue Oct 17, 2015 · 46 comments

Comments

@xpepermint
Copy link

I would need GraphQLUnionType to work also as an input type. I need to pass a list of different conditions to the server.

Example (start processing products where (A or B and C) - order is important):

mutation RootMutation {
   startProcessing({
      kind: "product",
      filter: [
         {kind: "A",  period: "1 min"}, // type1
         {operand: "or", kind: "B",  tag: "smart"}, // type2
         {operand: "and", kind: "C",  tag: "small"} // type2
      ]
   }){kind}
}
@xpepermint xpepermint changed the title GraphQLInputUnionType GraphQLUnionType as input type Oct 17, 2015
@leebyron
Copy link
Contributor

Interesting - I'd like @dschafer to see this as well.

I know at FB we've stayed away from this sort of input flexibility as it leads to more complex and easy to mess up server side implementations and in some cases can lead to ambiguities where it's not possible to determine which type in the input you meant, but I definitely understand why this could be valuable.

@xpepermint
Copy link
Author

Great!

@dschafer
Copy link
Contributor

Yeah, we've definitely seen cases where this would be useful. We've represented these as input objects that basically simulate tagged unions, and have helper methods internally that validate the inputs (that only one field on the tagged union is set).

@lolopinto has thought a lot about this, I'd like to get his thoughts, but this seems like an idea we should pursue. My initial thought is that a GraphQLUnionType that only contains input types would be considered a valid input type?

@xpepermint
Copy link
Author

Btw, having the interfaces concept for GraphQLInputObjectType would be great.

@sorenbs
Copy link

sorenbs commented May 25, 2016

We have a use case for this as well. We are building a generic CRUD GraphQL api. one of the operations we are supporting is linking two objects like this:

mutation {
  createUser(name: "carl", cityId: "cityId")
}

We would like to support creating the city inline like this:

mutation {
  createUser(name: "carl", city: {name: "Berlin", knownFor: "cool GraphQL people"})
}

Ideally we would like the type of the city input field to be {id!} || {name!, knownFor}. It seems like input Union Types would support this.

@fson
Copy link
Contributor

fson commented Jun 25, 2016

I've ran into another use case where unions for input types would be useful.

I'm adding a GraphQL mutation in Reindex for authenticating with a social access token (Facebook Login, Twitter etc). The problem is that different providers require different input parameters:

  • OAuth 2 providers like FB and GitHub only require an access token.
  • OAuth 1 providers, e.g. Twitter takes an access token, secret and user ID.

These difference mean that without union input types, I would have to create separate mutation for each login provider we support and plan to support in the future. This also wouldn't be very good for code reuse on the client-side, e.g. each provider would require a separate Relay mutation.

@leebyron mentioned union types can lead to ambiguities where it's not possible to determine the type. This gave me an idea: what if GraphQL input types would only support disjoint unions (like those supported in Flow)? This would avoid the potential ambiguities, yet allow expressing the input type for cases like I've described above like this:

union LoginInput = FacebookLoginInput | GithubLoginInput | TwitterLoginInput

input FacebookLoginInput = {
  provider: "facebook"
  accessToken: String
}

input GithubLoginInput = {
  provider: "github"
  accessToken: String
}

input TwitterLoginInput = {
  provider: "twitter"
  accessToken: String
  accessTokenSecret: String
  userID: String
}

It's possible to emulate these with nested input objects and runtime checks, as @dschafer described above, but being able to express them in the type system would enable better type checking and make the API more self-documenting than relying on out-of-band information ("always set only one of these fields").

@sorenbs
Copy link

sorenbs commented Aug 30, 2016

@leebyron Any chance this would be considered in the future?

@leebyron
Copy link
Contributor

Yes, I still think this idea is interesting, though the tricky part is definitely coming up with the right semantics and validation for disambiguating within input unions.

e.g.:

union MyUnion = Person | Place | Thing

input Person {
  name: String
  birthdate: Int
}

input Place {
  name: String
  lon: Float
  lat: Float
}

input Thing {
  name: String
  weight: Float
}

Should this be legal? If so, then what should GraphQL do with { name: "Santiago" }? Should that be legal? If so, what type does it use for coercion?

@leebyron
Copy link
Contributor

leebyron commented Aug 31, 2016

Also, one of the design principles for GraphQL is to bias towards simplicity. Any time an idea can be expressed using less concepts we should use less concepts, even if that means being slightly more awkward.

Biasing towards simplicity lowers the barrier to entry for other implementers or adopters of GraphQL, the value of which should not be underestimated.

For example, @fson's example above is perhaps a fine example of how input unions could be useful, however I could also represent the same thing with:

input LoginInput {
  provider: String! # or maybe an enum
  accessToken: String
  accessTokenSecret: String
  userID: String
}

Does that make it a bit harder to describe that userID is only necessary when provider is "twitter"? Yes, I think so. But is it simpler? Yes, definitely! One type instead of four and one fewer concept.

@leebyron
Copy link
Contributor

Part of the reason why this issue has not been a priority is there still is not a set of truly compelling examples where input unions would unlock the ability for GraphQL to express APIs that it cannot yet express today.

It's easy to come up with example use cases for any given idea, but as long as each of those examples can be reasonably represented with the existing features then they're not that compelling an argument for adding complexity.

@sorenbs
Copy link

sorenbs commented Aug 31, 2016

Thanks Lee!

I appreciate the strive for simplicity and see how allowing unions of types that can not be disambiguated is a problem.

The primary value union input types would unlock for us is being able to express in the typesystem that exactly one of two fields has to be supplied. As is the best we can do is make both fields optional and return a runtime error if none or both is supplied. (as in my example above)

It might be that union input types is not the best way to express this and I would be happy to consider other existing or potential constructs we could use to achieve the same .

@BerndWessels
Copy link

+1
I have different types of filters, like string, number, date, regexp, set and so on. And I want to be able to have an Input Union Type across all of these types to pass them as arguments to my query.

@fubhy
Copy link

fubhy commented Sep 5, 2016

I'd also like to see this happen. For now, I am using https://github.com/Cardinal90/graphql-union-input-type as a temporary solution.

@tomparkp
Copy link

tomparkp commented Oct 5, 2016

I was considering this approach for supporting different types of pagination. We have customers that have varying technical capability and needs so the idea was to accept a couple types like OffsetConfig | CursorConfig | TimeConfig for a PaginationConfig field.

Though doable the way @leebyron describes it seems like it would be pretty messy and make validation more difficult to have all the possible fields in one config object.

I understand the simplicity argument, but I think you could argue its more simple to have a smaller set of types work universally across inputs and responses, etc. I went forward doing it without a second thought and was surprised when it didn't accept it.

@emrul
Copy link

emrul commented Oct 19, 2016

I'm relatively new to GraphQL so forgive my naivety but I don't understand why we have to explicitly define 'InputTypes' versus being able to set an 'isInputType' (boolean) property on any type definition (be it ObjectType, UnionType, etc.). It would be simple(r) all round and shouldn't mess up parsing & validation.

@kapouer
Copy link

kapouer commented Oct 30, 2016

@richburdon
Copy link

Hi @leebyron

Biasing towards simplicity

Understood, but couldn't you apply this principle to not having unions in the first place? (And your argument to merge @fson's login types above)?

Unions are useful -- and of course the thread applies to interfaces as well!

I'm using https://github.com/Cardinal90/graphql-union-input-type (thanks @fubhy for the link) but it adds considerable complexity to rely on third-party extensions.

there still is not a set of truly compelling examples...

Suppose you have heterogeneous objects that can be fields of a common container data type.

union DataUnion = Contact | Event | Org | Task | Project | Document

type SearchResult {
title: String!
data: DataUnion!
}

How would I create a common mutation to create a search result? Our current approach is to convert all of the union subtypes to be Nodes and pass the ID as part of the SearchResult? But is this example any more compelling.

Thanks very much.

@theartoflogic
Copy link

Part of the reason why this issue has not been a priority is there still is not a set of truly compelling examples where input unions would unlock the ability for GraphQL to express APIs that it cannot yet express today.

I don't know if this is compelling or not, but one use-case I have is to be able to define a union type for an input argument used for filtering results.

The reason a union input type would be useful is because I have implemented advanced operators besides just a simple equality match, but this has required me to make simple equalities more complicated. How I've done this is allow the user to specify an object where each key is an operator and the value is the value to apply to that operator. For example:

{
    "filter": {
        "numFlags": { "gt": 0 }
    }
}

This would tell the query to search for items that have a numFlags field that is greater than 0 (the syntax is similar to MongoDB). To accommodate simple equalities, I've added an eq key:

{
    "filter": {
        "numFlags": { "eq": 0 }
    }
}

This makes doing simple equalities a little more verbose than being able to simply specify:

{
    "filter": {
        "numFlags": 0
    }
}

Therefore, preferably, I'd be able to specify the numFlags argument as a union type of: GraphQLInt | ComplexFilter, where ComplexFilter is a GraphQLInputObjectType that contains the various operators that can be applied to that field.

This isn't a showstopper for me, as I do have the workaround of using the eq operator for simple equalities. But it'd be nicer to be able to support a simple key/value pair for the most common use-case of simple equalities. I've read some other proposals to handle operators, such as defining additional fields such as numFlagsGreaterThan and such, but I don't really like polluting my argument list with a bunch of these operator-named arguments.

@bgentry
Copy link

bgentry commented Feb 2, 2017

I'd love to see this feature. Here's my use case:

What I'm building is conceptually similar to IFTTT. Imagine trying to create IFTTT Applets, which can be for any number of services, all of which take different options and associate with different entities in the system. As I understand it, my current choices are:

  1. Eschew most of the benefits of GraphQL by just accepting an opaque JSON object type in my input, i.e. options, and then going through a complicated process on the backend to ensure that the options provided are correct for the specific type of service being integrated with.
  2. Add dozens or hundreds of optional arguments to the endpoint that creates the Applet. These have the benefit of being typed, but it is entirely unclear to the client which options are needed when. The API would also get bloated really quickly.
  3. Add separate endpoints for each type of Applet / service that can be created. Each endpoint would give me access to all the functionality and benefits of GraphQL, but my API would have a huge number of mostly-redundant APIs.

The issue isn't just on creation of these entities. I'd also need redundant APIs for update mutations. On queries, I can model this perfectly using Interface and/or Union types. But there's a huge mismatch between the types that the server can render and those that it can accept as input.

I'd really like to see another option where I can have a single API endpoint that is properly typed, and the input args can just be required to fit a certain Interface type, or at least a Union type.

I know there's a desire to avoid additional syntax for specifying which type of input is actually being provided. The best answer I could come up with there is for the query to specify the concrete type being provided rather than the interface type. But that does mean client queries are no longer completely static in my case, or I'd need a separate query for every type of input I might provide. Maybe others have ideas.

@mike-marcacci
Copy link
Contributor

Custom scalars are definitely a valid workaround, but make sure they don’t invalidate any security assumptions you’ve made (check out this great post on potential security pitfalls).

@peterwj
Copy link

peterwj commented Dec 9, 2017

Hey all! I've got a similar situation on my hands where a union input type would be handy. In pseudocode using no particular syntax, the schema of the object I wish to upload via mutation looks something like this:

Mutation: {
  dueDate: date,
  commodity: string,
  weight: float,
  legs: List<RailLeg | TruckingLeg>,
}
RailLeg: {
  port: ...,
  inlandPort: ...,
}
TruckingLeg: {
  port: ...,
  deliveryAddress: ...,
}

Basically, I've got a list of heterogeneous entries that together form a sensible collection, but whose internal structure differs. I'd like to use GraphQL to enforce that each member of that list can be typed according to one branch of the union type, but without union type support as inputs, I cannot.

However, I've found a good substitute that may be useful to other Ruby backend users. I wrote the GraphQL input type as the union of all possible keys for all possible union type branches (currently 13 distinct keys covering 5 different branches in my real project), and I'm running each member of the list through a dry-struct type, one struct type per branch, to enforce schema compliance. It's not ideal because I have to list the struct members twice – once as the GraphQL input type and once as the dry-type – but it's been working quite well.

@jasonbahl
Copy link

It seems like the pattern for the web at large is to have more controlled, structured inputs for html content, but there’s not really any good way to validate blocks of html input right now. GraphQL input unions would allow for structured inputs AND true server side validation on the “blocks” as they are stored and converted into something like html.

WordPress, for example, is in the process of building a new Block Based editor, “Gutenberg” and I would love to be able to have WPGraphQL (which I maintain) integrate with the new block-based Gutenberg editor.

The blocks in Gutenberg can be registered with various inputs, and at the moment all block input gets converted to one big string and is saved, with no real block level validation or real way to interact with a single block.

Ideally, with a block based editor like Gutenberg, each block should have its own schema for querying, but also for mutating.

The query part is easy with unions, as each block that was saved is of a specific block type. . .but without input unions, the mutation becomes extremely difficult.

The client knows which type (__typename) each block is, so when mutating it would be easy for the client to pass back the block’s typename along with whatever other input goes along with the block.

Ideally, for WPGraphQL + Gutenberg (and it seems like many other projects) it would be sweet to be able to do mutations with a listOf InputUnions.

Looking forward to what comes of this!

@tgriesser
Copy link
Contributor

Just opened a PR proposing an inputUnion type - would love to get feedback from anyone on the idea!

@mike-marcacci
Copy link
Contributor

@tgriesser it looks like there's a related issue on the GraphQL spec but no actual RFC. I'll bet that any insight you gained while working on your implementation would be helpful over there as well!

@hasyee
Copy link

hasyee commented Mar 4, 2018

Why cannot we use resolveType or isTypeOf method to resolve the input polymorphism, similar to output polymorphism? I think that the most relevant way should be giving chance to the developer to decide which type would they cast.

resolveType?: (value: any, info?: GraphQLResolveInfo) => ?GraphQLObjectType;

In most of cases we use some meta-property for distinguish polymorphism, and usually we persist this property in database too, so a resolveType implementation would be easy, and even developers can choose a fallback strategy too:

resolveType: value => {
  switch (value.type) {
    case 'A': return A;
    case 'B': return B;
    default: return A;
  }
}

or

isTypeOf: value => value.myFunnyType === 'A'

or

isTypeOf: value => 'xyz' in value

I think, GraphQL is really powerful at the really complex data models, and complex data models can be strongly polymorph. I love GraphQL, but the lack of this feature is disincentive for me to use GraphQL for my complex projects.

@konsumer
Copy link

konsumer commented Mar 27, 2018

I end up solving this more like protobuf, where I make a mega-type that has all the children, then just fill in the one I care about:

enum RecordType {
  User
  App
  Search
  Experiment
}

union AdminItem = User | App | Search | Experiment

type AdminList {
  results: [AdminItem]
  stats: QueryStats
}

input AdminItemInput {
  User: UserInput
  App: AppInput
  Search: SearchInput
  Experiment: ExperimentInput
}

type Query {  
  # Get a single record.
  adminGet(id: ID!, type: RecordType!): AdminItem
  
  # List records of a specific type.
  adminList(type: RecordType!, start: Int, count: Int): AdminList
}

type Mutation {
  # Delete a record.
  adminDelete(id: ID!, type: RecordType!): Boolean
  
  # Create/update a record.
  adminEdit(record: AdminItemInput!): AdminItem
}

Then in my resolvers, I do something like this:

export default {
  Query: {
    adminGet: (_, {id, type}, {models}) => models[type].get(id),

    adminList: (_, {type, start, count}, {models}) => models[type].findAll({}, start, count)
  },

  Mutation: {
    adminDelete: (_, {id, type}, {models}) => models[type].del(id),

    adminEdit: async (_, {record}, {models}) => {
      const k = Object.keys(record)
      if (k.length > 1) {
        throw new Error('You may only edit one record at a time')
      }
      const type = k.pop()
      let oldRecord = {}
      if (record[type].id) {
        oldRecord = await models[type].get(record[type].id)
        if (!oldRecord) {
          throw new Error('Item not found')
        }
      }
      const newRecord = {...oldRecord, ...record[type]}
      await models[type].save(newRecord)
      return newRecord
    }
  },

  AdminItem: {
    __resolveType: (obj, context, info) => {
      if (obj.email) {
        return info.schema.getType('User')
      }
      if (obj.term) {
        return info.schema.getType('Search')
      }
      if (obj.storeID) {
        return info.schema.getType('App')
      }
      if (obj.object) {
        return info.schema.getType('Experiment')
      }
      return null
    }
  }
}

AdminItem.__resolveType can tell the difference between User | App | Search | Experiment, so it works ok. This seems like a bit of a hack, and it'd be nice to be able to just say "it's one of these types for input" like we say "it's one of these types for output". Also, if I need to go very deep, it gets super-messy, fast.

@konsumer
Copy link

konsumer commented Mar 27, 2018

Note: I left out the User | App | Search | Experiment and UserInput | AppInput | SearchInput | ExperimentInput definitions

Also, it has a few assumptions that if not correct would screw everything up:

  • I can figure out AdminItem from the field-values (this could also be a type field or something)
  • My models in context are named the same as the types (User | App | Search | Experiment)
  • Every record has an id field

@jturkel
Copy link

jturkel commented Apr 25, 2018

To add another perspective to this thread, for me lack of input union support is less about enabling something that isn't possible with GraphQL via workarounds but more about removing objections to migrating the Salsify APIs from REST to GraphQL. One of the really appealing things about GraphQL is the type system and everything it enables - client side type checking, better API documentation, auto-complete in GraphQL editors, framework validation of incoming requests, etc. This has the potential to really improve our internal developer productivity and make it much easier for customer/partner developers to use our APIs in their preferred language/framework with minimal friction. Unfortunately we have several examples in our domain (product information management and syndication) where an entity has an attribute that is a collection of structured values with heterogeneous types and our save UX/desired API usage calls for saving the entire entity atomically.

Here's a simplified version of one such example representing the configuration of a product catalog that has an ordered collection of sections which can display an image carousel, a list of products or a spotlight for an individual product:

type Image {
  # ...
}

type ImageCarousel {
  images: [Image!]!
}

type ProductList {
  productFilter: String!
  productProperties: [Property!]!
  productsPerPage: Int!
}

type ProductSpotlight {
  product: Product!
  productProperties: [Property!]!
  numDisplayedImages: Int!
}

union Section = ImageCarousel | ProductList | ProductSpotlight

type Catalog {
  id: String!
  name: String!
  sections: [Section!]!
}

type Query {
  catalog(id: String!): Catalog
}

input ImageCarouselInput {
  imageIds: [String!]!
}

input ProductListInput {
  productFilter: String!
  productPropertyIds: [String!]!
  productsPerPage: Int!
}

input ProductSpotlightInput {
  productId: String!
  productPropertyIds: [String!]!
  numDisplayedImages: Int!
}

inputUnion SectionInput = ImageCarouselInput | ProductListInput | ProductSpotlightInput

type CatalogInput {
  id: String!
  name: String!
  sections: [SectionInput!]!
}

type Mutation {
  updateCatalog(catalog: CatalogInput): Catalog
}

schema {
  query: Query
  mutation: Mutation
}

Reading through this issue it looks like we've got a few potential workarounds:

  1. Make SectionInput a tagged union with fields for imageCarousel, productList and productSpotlight
  2. Make SectionInput a union of all the fields on ImageCarouselInput, ProductListInput and ProductSpotlightInput
  3. Don't try to model SectionInput with the GraphQL type system and use a JSON literal instead
  4. Use schema directives to simulate input unions
  5. Relax our requirement that all sections in a catalog can be updated atomically and expose mutations like setImageCarouselSection(catalogId: String!, sectionIndex: Int!, imageCarousel: ImageCarouselInput!)

Option 5 is a non-starter since it doesn't fit with how our consumers actually want to use our API.

Option 4 is intriguing but we'll have minimal control over the tool chain used by 3rd party developers which makes adopting anything non-standard less appealing.

Options 1, 2 and 3 boil down to the same problem: the GraphQL type system wouldn't actually describe what's possible with our API, which is one of the main reasons we were considering adopting GraphQL to begin with. Anything in the tool chain that relies on schema introspection (e.g. auto-complete in GraphQL editors, linting of client queries, etc.) won't represent the "real" type system of our API which will hurt the productivity of both internal developers and may lead to 3rd party developers not using our APIs. Furthermore we're stuck with a difficult decision when it comes to queries and mutations: we're either inconsistent between the two (beyond the usual GraphQL conversion of nested entities to foreign keys), or we're consistent between them, avoid the use of unions on the query side, and then we have the same set of issues with developer ergonomics and the client toolchain there as well.

At the end of day, it's going to be much harder for me to sell a GraphQL migration internally unless I can demonstrate a big improvement in developer experience over our current REST APIs, which is going to be tough without input unions to model some really important constructs in our domain.

@vieks
Copy link

vieks commented May 4, 2018

Here you guys are facing to a huge design misconception | internal flaw to the assumption made by the GraphQL authors:
validation = typing

All the workarounds posted here are ugly.
You can't patch|fix a bad language design by code at least at the price to get some even uglier and unmaintainable code base.

Maybe a code generation tool for GraphlQL will be OK to handle & hide this.
But handwritten GraphQL server will not.

@orefalo
Copy link

orefalo commented May 4, 2018

GraphQL authors: validation = typing

agreed on this one.. simplicity helps adoption but it has its limits.

@IvanGoncharov
Copy link
Member

This discussion is very important not only for graphql-js but for the entire GraphQL ecosystem so it should be discussed in the context of GraphQL specification.
Please discuss it in: graphql/graphql-spec#488

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests