-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Comments
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 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.
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. Input types represent set of data required for a particular action or query (e.g. 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:
So instead of creating two additional types and complicating type system with
IMHO, So it should be either in place defined strings or enum values. |
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 inputUnion SectionBlockInput = ImageGalleryInput | HighlightedImageInput | ArticleListInput | RichTextInput I would skip the "super"
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:
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. |
@frikille Thank you for sharing your use case it helped me a lot with understanding your particular problem. |
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. |
Creating/updating an entity ( 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. |
@frikille @jturkel Thanks for more examples I will try to study them this weekend. 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. @mjmahone Maybe you know, can something like this affect Facebook codegen for iOS and Android? |
@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:
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 |
@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 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 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. :) |
@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 input Foo {
kind: "Foo"!
# ...etc
} ??? |
@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. |
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:
|
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. |
@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. |
@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 |
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 🤔 |
@IvanGoncharov - The proposed input union functionality for strongly typed clients should behave much the same way as "output" 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). |
@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 :) |
@jturkel Agree and thanks for pinging 👍
Working group don't have fixed seats and if you want to raise or discuss some issue just add it to the agenda.
I can help with the spec and So as a next step I think we should find some points that we all agree on. |
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. |
@jturkel Update: Have an urgent work so I wouldn't be able to compile something until next weekends. |
Any updates on this? We are facing a similar need, unless there's a better way one may suggest:
|
@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
} |
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
So... no input union types then? |
It appears not. :( |
edit: #825 OneOf is successor to this spec |
Thanks! I'll take a look at it :) |
Did you manage to solve your issue ? |
For those seeking alternatives, please consider |
If, like me, you need a quicker fix than 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 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!]!
# ...
} |
Hi, |
@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? |
@brilovichgal dunno… IMO, |
I extremely object with the decision to use |
input RecurrenceOption @oneOf {
typeOnly: RecurrenceType
typeAndEndDate: RecurrenceTypeAndEndDate
typeAndOccurrences: RecurrenceTypeAndOccurrences
}
input RecurrenceTypeAndEndDate {
recurrenceType: RecurrenceType!
endDate: Date!
}
input RecurrenceTypeAndOccurrences {
recurrenceType: RecurrenceType!
ocurrences: Int!
} |
@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. |
Since all types have the input RecurrenceOption {
recurrenceType: RecurrenceType!
extra: RecurrenceExtra # Note that this is nullable, thus optional
}
input RecurrenceExtra @oneOf {
endDate: Date
occurrences: Int
}
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? |
[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 @tgriesserRFC 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.Tagged union by @leebyron and @IvanGoncharov
Original comment
Directive
Original comment
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:
With that our GraphQL schema definition would be the following (Full schema definition)
And a mutation query would be the following:
or with variables
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:
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
Cons
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.
The text was updated successfully, but these errors were encountered: