-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Comments
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. |
Great! |
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 |
Btw, having the |
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:
We would like to support creating the city inline like this:
Ideally we would like the type of the city input field to be |
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:
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"). |
@leebyron Any chance this would be considered in the future? |
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 |
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 |
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. |
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 . |
+1 |
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. |
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 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. |
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. |
@emrul it's nicely explained http://graphql.org/graphql-js/mutations-and-input-types/ |
Hi @leebyron
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.
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 { 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. |
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 {
"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 This isn't a showstopper for me, as I do have the workaround of using the |
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:
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 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 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. |
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). |
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:
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 |
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! |
Just opened a PR proposing an inputUnion type - would love to get feedback from anyone on the idea! |
@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! |
Why cannot we use
In most of cases we use some meta-property for distinguish polymorphism, and usually we persist this property in database too, so a 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. |
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
}
}
}
|
Note: I left out the Also, it has a few assumptions that if not correct would screw everything up:
|
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:
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. |
Here you guys are facing to a huge design misconception | internal flaw to the assumption made by the GraphQL authors: All the workarounds posted here are ugly. Maybe a code generation tool for GraphlQL will be OK to handle & hide this. |
GraphQL authors: validation = typing agreed on this one.. simplicity helps adoption but it has its limits. |
This discussion is very important not only for |
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):
The text was updated successfully, but these errors were encountered: