-
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
allow unions to declare implementation of interfaces #939
base: main
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for graphql-spec-draft ready!
To edit notification comments on pull requests, go to your Netlify site settings. |
- Spec PR: graphql/graphql-spec#939 - graphql-js PR (WIP): graphql/graphql-js#3527
* add agenda item for unions implementing interfaces - Spec PR: graphql/graphql-spec#939 - graphql-js PR (WIP): graphql/graphql-js#3527 * Update agendas/2022/2022-04-07.md Co-authored-by: Benjie Gillam <benjie@jemjie.com>
see: #518 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am really excited for this to become part of the spec. Thanks so much for getting the work done here! This looks great so far. Interested to see what the working group has to say about it!
@AnthonyMDev thanks for the comments! I think you are right on. I also have some changes coming in to the validation edits to reflect how the PR came together. namely, when validating interface use with unions, we can check simply whether member types explicitly declare that they implement those interfaces and stipulate that the interface implementation for the member types has been validated |
Comments from WG: Unions don't implement interfaces, their member types do. This keyword may be helpful to create a contract by which the union declares that its member types all implement an interface so that queries can be reliably written to rely on that fact. Otherwise, new member types could be added that don't implement the interface and it would be unfortunate if adding a new member type would break existing queries in that way. Explorations:
There were some interesting views on the third point. Some exploration on the third point within the reference implementation yielded that queries of this type would execute anyway without a problem, but fail validation because of the |
In terms of the first point, I imagine we could think along the lines of a new intersection type that would operate on Union and Interfaces types. It would contain only the object types in the unions but only if they implement all of the interfaces. Aside from preferred syntax concerns, what would the practical difference be? I can't think of one (yet). Feedback welcome! |
@leebyron does that feel like the correct precedent from other type systems? |
Specific keyword aside, imagine you have the schema: interface Foo {
foo: Int
}
interface Bar {
bar: Int
}
type A implements Foo & Bar {
foo: Int
bar: Int
a: Int
}
type B implements Foo & Bar {
foo: Int
bar: Int
b: Int
}
type C implements Foo & Bar {
foo: Int
bar: Int
c: Int
}
union AOrB implements Foo & Bar = A | B
type Query {
q: AOrB
} I think we're all agreed during the WG call that the following query should be valid: {
q {
foo
bar
... on A { a }
... on B { b }
}
} For completeness, I think the following query should not be valid: {
q {
foo
bar
... on A { a }
... on B { b }
... on C { c }
}
} But the following should be valid: {
q {
foo
bar
... on A { a }
... on B { b }
... on Foo {
... on C { c }
}
}
} I think to implement this we need:
|
I think the latter is also done now but I may have missed a case, will have to double check, if you can point to more areas to update, of course that would be great. |
But for completeness, I think the question is whether you should be able to do ... on AorB |
@benjie |
@rivantsov It should be valid because it is currently valid on the underlying union (ignoring the new |
See graphql/graphql-js#3550 which introduces a new The practical difference is that intersections are more powerful, especially if we follow this up with the expansion of Unions to include abstract types such as other unions, intersections, interfaces... |
spec PR: #941 The main practical difference after further reflection seems to be how to handle adding a new type to the union that does not implement the interface. Using intersections, it's not a problem. Using unions that implement interfaces, you are kind of stuck. |
I don't get it - 'you are kind of stuck'. What is the problem of adding a type to union, when type does not implement interface? - this is a clear fault. That's a violation of the promise (union implements interface means all concrete types implement it), so it is an error, at init time should be rejected by schema validation code. That's the whole point! I do not see it as a problem. |
@rivantsov It's about flexibility. We the intersection type you can combine types more flexibly and schema evolvability. @yaacovCR I think we should draw up some real use cases that show the problem we are trying to solve. Maybe we even can have a look at a couple of approaches to solving these use cases in an RFC document like we did for input unions. |
Please see discussion linked above. It has the real world use case with unions implementing node linked above. I think if we think of a third solution it makes sense to have a separate RFC doc, but right now we are just comparing the two. We could always add an additional option to avoid the use of unions in favor of interfaces, but that does not seem truly on point to me. Seeing as unions exist, they could exist as fields on types that implement an interface... |
Decision Record We decided at the last WG that if unions implement interfaces, then they have directly querable fields. This PR and the implementation PR have been updated with the changes for introspection, validation. |
Note that we currently avoid direct validation that the implementations declared by the union do not conflict, i.e., the following will yield a validation error for the empty union SomeUnion implements SomeInterface & SomeClashingInterface
interface SomeInterface {
commonField: String
}
interface SomeClashingInterface {
commonField: ID
} Once an object is placed in the union, it won't be able to fulfill both interfaces without an error. That works for me, but I could anticipate feedback that we should have independent validation, so if that's the view, I could try to implement that prior to the next WG. |
Any independent rule would have to note that the following is valid: union SomeUnion implements SomeInterface & AnotherInterface
interface SomeInterface {
commonField: String
}
interface AnotherInterface {
commonField: String!
} |
My first pass of the algorithm for implementing the independent validation rule would be something like:
|
Two additional thoughts:
Consider the following initial types: interface Pet {
name: String
friends: [Pet]
}
type Cat implements Pet {
name: String
friends: [Pet]
}
type Dog implements Pet {
name: String
friends: [Pet]
}
union CatOrDog implements Pet = Cat | Dog
query {
catOrDogs: [CatOrDog]
} The {
catOrDogs: {
name
friends {
name
}
}
} But what happens if we add another interface to the schema:
And it all works! Because the type signature of CatOrDog is essentially equivalent to the following pseudo-interface: union CatOrDog implements Pet & Named & Friendly = Cat | Dog : {
name: String
friends: [Pet]
} But what if we add an interface with an overlapping field name with a slightly different type?
But what is the field type for I think the breaking change surface area should be as narrow as possible, so I don't think we should allow automatic assignment of fields to union that implement interfaces. But opinions may differ! Let's discuss at the next WG |
@yaacovCR - I do not understand anything. In my understanding: |
The same name fields are of different but overlapping types, so I do not think they collide except when trying to automatically assign fields to the Union. Similar to the now defunct intersection idea, "automatic" things in graphql cause problems. |
@yaacovCR Sorry, I got a little lost trying to follow your argument, please can you write just the final full schema (i.e. without the use of the If you have |
I guess this is where I messed up, it's actually of type "Friendly & FriendlyWithoutGreeting," so everything should be ok? I guess where I'm getting confused is defining an algorithm for Well, where is It's possibly that sharper minds than mine might be required for this. :( |
If
I don't believe that statement is correct. In your final schema you have:
In the end
I see no contradiction between |
Thank you so much @rivantsov @benjie for your feedback. I have tried to throw additional edge cases at this, and have failed to create a breaking change problem, although there is still an implementation difficulty I am hitting in terms of how to calculate the introspection results: For the following schema (and note that the important part is the union toward the bottom): interface SomeInterface {
id: ID
children: [SomeInterface]
someField: String
}
interface AnotherInterface {
id: ID
children: [AnotherInterface]
anotherField: String
}
interface CommonInterface implements SomeInterface & AnotherInterface {
id: ID
children: [CommonInterface]
someField: String
anotherField: String
}
type SomeType implements CommonInterface & SomeInterface & AnotherInterface {
id: ID
children: [CommonInterface]
someField: String
anotherField: String
someUniqueField: String
}
type AnotherType implements CommonInterface & SomeInterface & AnotherInterface {
id: ID
children: [CommonInterface]
someField: String
anotherField: String
anotherUniqueFIeld: String
}
# The below is the important part
union SomeUnion implements CommonInterface & SomeInterface & AnotherInterface = SomeType | AnotherType
type Query {
rootField: [SomeUnion]
} The fields directly available on the union, because of {
rootField {
id
children {
id
children {
...
}
someField
anotherField
}
someField
anotherField
}
} meaning, the available result shape of the union is equivalent to the following interface that defines no new fields: interface InterfaceEquivalentForUnion implements CommonInterface & SomeInterface & AnotherInterface {
id: ID
children: CommonInterface
someField: String
anotherField: String
} If Unions have fields, then introspection must return those fields, i.e. the fields of the equivalent interface. So how can the implementation determine that algorithmically? Some static analysis can determine that Options:(1) Anonymous interfaces One way might be to introduce the idea of an "anonymous interface". We can then rewrite the equivalent interface as follows, by following a simple algorithm where we collect all fields from the interfaces that the union implements, and where there is an overlap, introduce the intersection, as follows interface FieldsForUnionInterspectionResult {
id: ID
children: CommonInterface & SomeInterface & AnotherInterface
someField: String
anotherField: String
} If there was an anonymous interface type allowed, then introspection could just return the above directly.... (2) Static analysis? Perhaps the implementation could generate the anonymous interface above, and then search for a type that fulfills that interface, which does and I guess must (?) exist, and in this case, of course, is How can we get from A more general algorithm might be that if any two types on this list are subtypes of one another, pick the subtype? I am not sure how to show/prove that these algorithms work. (3) Not (yet?) allow fields to be queried directly on unions Meaning, my primary use case was to enable unions fields to fulfill an interface field, avoiding a wrapping fragment could be a separate feature that we could work on later. |
Question: does the introspection have to give fields to the union? I can certainly see the value in doing so, but it also seems an amount of redundant data since the interfaces already include those fields and the union is not adding any additional information to them - in particular the union does not declare those fields in SDL, so adding them to introspection would break the symmetry between introspection and SDL. Note this is not the same situation as interfaces and a type declaring the same fields, because in that case the type and interface fields may differ subtly so long as they're compatible; for a union implementing interfaces the fields are from the interfaces itself - the union doesn't really "have" fields. I think the topic of adding fields to the introspection for unions is independent from the topic of resolving those fields during |
Thanks for that awesome point! I guess how we want introspection to work is a good discussion for the working group. I thought giving unions fields would be across the board, but I see your point! By the way, spec says that for object types, fields is the "available" fields, while for interfaces it is the fields "required" by the interface. So there is already some ambiguity there, tending perhaps in your direction, if could have used "available" for both? As an aside, having the fields actually defined on the Union helps with validation, not execution. getFieldDef in execute is only passed the concrete type. Even if we decide that we don't want to return fields for unions during introspection, I am clearly having trouble with the implementation for validation that needs these pseudo fields. Please anyone feel free to reach out with any tips, or post to implementation PR |
Linking the ImplicitInheritance RFC here as it seems (loosely at least?) related to this issue? If:
Then sounds like that would solve automatically the problem of unions implementing interfaces and not declaring any fields? Of course that's a huge change so not sure how doable that is (certainly not too much) but feels like something that's somewhat related |
Very relevant. Basically, I did not solve this part:
|
What is the status on this @yaacovCR? I know there were a few unresolved questions left. I haven't seen any action on this in a while though. Are you still working on getting this ready to be approved? Are there blockers, that need to be unblocked? Or has this taken a back seat for now? I'd really love to see us move forward on getting this into the working draft of the spec, but I understand the time and effort needed for that is significant. I'm not in a huge rush for this to happen ASAP, but I would be disappointed to see it never completed. If there is anything that I can do to help move this forward, please let me know! |
The basic unresolved question is whether and how unions that are constrained so that their member types all implement an interface will allow querying of those interface fields directly on the union. This is straightforward in my canonical case of a union where all members implement the
Yes, I kind of hit a wall. We could try just not allowing that additional client-facing feature and just introducing the constraint. From what I recall, you were actually more interested in that client feature. I am not sure why that is so helpful to you => I wonder if what you are really looking for is for unions to be allowed to include interfaces, which seems like a more useful feature from the aspect that I recall you discussing, which is the annoyance of adding new types to unions. It seems like if interfaces could be members of unions, it may be somewhat common to add new implementations of interfaces that are already in the union, rather than a new union. Although maybe you could reiterate here your more direct use case.
Kind of! I moved on to exploring other areas of bolstering subtyping in GraphQL. You can follow my stumbling efforts in a few riveting portions of recent GraphQL WG meetings. Inspired by your prompt here, I have summarized my research/exploration here: https://github.com/graphql/graphql-wg/blob/main/rfcs/ExpandingSubtyping.md -- please feel free to suggest changes/additions to that RFC if you are so inclined, especially in terms of motivations.
I touched on the main blocker for unions declaring implementation of interfaces above. The blockers for unions including other abstract types was the WG's general inclination that the constraint should be listed on the child union, while my contention that we should allow abstract types to be actual members of the union; i.e. a more major change to GraphQL than the simple addition of a constraint. We also discussed whether there would be any problems with (too much?) recursiveness during schema construction; on further reflection, this seems to be a larger problem with unions that contain unions rather than unions that contain interfaces; the latter seems less problematic in terms of recursiveness, as well as more useful!
I don't think I made enough progress since last meeting to really discuss this in depth at tomorrow's meeting, but I can point to the new RFC and ask for feedback. Your feedback, especially your use cases, would definitely be appreciated. |
* add agenda item for unions implementing interfaces - Spec PR: graphql/graphql-spec#939 - graphql-js PR (WIP): graphql/graphql-js#3527 * Update agendas/2022/2022-04-07.md Co-authored-by: Benjie Gillam <benjie@jemjie.com>
Cf. #373 (comment)