ghostdogpr / caliban

Functional GraphQL library for Scala
https://ghostdogpr.github.io/caliban/
Apache License 2.0
947 stars 248 forks source link

Force a sealed trait to not be an enum #928

Closed rtimush closed 3 years ago

rtimush commented 3 years ago

Currently, if all children of a sealed trait are objects, it is rendered as an enum in the schema. It would be nice to be able to add an annotation to render it as a union. Otherwise, if the trait is later extended with another class that has some fields, the schema changes in an incompatible way.

ghostdogpr commented 3 years ago

The problem is that unions do not support empty objects. For that reason when you mix case objects anc case classes we currently do the dirty trick of inserting a "fake" field named _ that returns a Boolean. A solution would be that you add such field in the enum directly so that's it's automatically considered a union for the same result (need at least one field anyway).

If you don't want to modify the initial trait, another way is to build a custom schema reusing the existing one. toType is quite simple to implement (there's a helper makeUnion that you can use), you just convert the enum type given by the autoderived schema to a union.resolve is same whether it's a union or an enum, so you can call the original one. Would that work for you?

rtimush commented 3 years ago

Both options unfortunately look not very pleasant.

Moving the fake _ field to the model is quite invasive, I'd prefer it to stay in the caliban layer, and not leak to the business code. Also, it seems that I'd have to add it not only to the base trait but also convert all objects to case classes with this _ field.

Implementing a custom derivation function is quite verbose, and unless I fully replicate the SchemaDerivation logic, I will have to remember to switch between this custom derivation and the standard one whenever the type hierarchy changes.

My use-case is something like this approach to have mutation errors typed:

union DoSomethingResult = ErrorA | ErrorB | DoSomethingSuccess

type Mutation {
  doSomething(arg: String!): DoSomethingResult!
}

While often at least one of the result types will have fields, initially they may be all empty. However, I need to render them as a union from the very beginning to keep the schema extensible.

Looking at the SchemaDerivation code, adding a GQLUnion annotation that takes precedence over auto-detection of the unions, seems to be pretty straightforward. I can submit a PR if you like. Otherwise, I guess a fully custom derivation logic is my only option.

ghostdogpr commented 3 years ago

Yeah, an annotation works. It'll be similar to the existing GQLInterface.

I think the custom schema isn't that complex: no need to deal with magnolia code, it's mostly a mapping between 2 Type objects. But it's true that you need to call it explicitly for the types that need it. I can show a draft of this solution if you're interested but I'm 100% okay with the annotation so you can go for it 😄