ghostdogpr / caliban

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

Is it possible to create parameterized field with Caliban? #405

Closed szymon-rd closed 4 years ago

szymon-rd commented 4 years ago

I have a following schema:

  case class GetAParams(i: Int)
  case class GetBParams(i: Int)
  case class Query( getA: GetAParams => Task[A] )
  case class A( getB: GetBParams => Task[B]) )

Then I apply it to the graphQL(RootResulover(Query(...))) and I get following error:

Cannot find a Schema for type acrimony.graphql.ApplicationSchema.Query.
     Caliban derives a Schema automatically for basic Scala types, case classes and sealed traits, but
     you need to manually provide an implicit Schema for other types that could be nested in acrimony.graphql.ApplicationSchema.Query.
     If you use a custom type as an argument, you also need to provide an implicit ArgBuilder for that type.

And I wonder - is it possible to achieve what I've written there? I.e. type A with parameterized getB field.

ghostdogpr commented 4 years ago

Yeah, that shouldn't be a problem. I just tried the following and it compiles:

import caliban._
import zio.Task

object Test {
  case class B()
  case class GetAParams(i: Int)
  case class GetBParams(i: Int)
  case class Query(getA: GetAParams => Task[A])
  case class A(getB: GetBParams => Task[B])

  val api = GraphQL.graphQL(RootResolver(Query(???)))
}

You didn't say what is B though, so there might be something else causing the compile error. One way to find out is to call Schema.gen directly on your case classes, which gives a more detailed error message about what's missing, like:

Schema.gen[A]
Schema.gen[B]
Schema.gen[Query]
szymon-rd commented 4 years ago

Ok, so that's the full example that reproduces the error:

  case class B()
  case class GetAParams(i: Int)
  case class GetBParams(i: Int)
  case class Queryp(getA: GetAParams => Task[A])
  case class A(getB: GetBParams => Task[List[B]])

  Schema.gen[Queryp]

I missed the List[B] part.

ghostdogpr commented 4 years ago

I see. That is something that happens when there is too much nesting: Magnolia (the library that derives the schema for your types) has trouble deriving everything at once and needs a little help.

Adding this will solve it:

implicit val schemaA = Schema.gen[A]

That way, when it derives the schema for Query, is already has the intermediate schema for A and it makes its job easier. I usually do this (see here) as it can reduce compile times and also give better error messages.

szymon-rd commented 4 years ago

Thank you, but I also missed one thing: The type B is recursive, and has reference to UIO[A], and that gives following error:

[Error] ... magnolia.Deferred is used for derivation of recursive typeclasses
ghostdogpr commented 4 years ago

In that case you should make the intermediate schema a lazy val.

Example:

import caliban.schema._
import zio.Task

object Test {
  case class B(getA: GetAParams => Task[A])
  case class GetAParams(i: Int)
  case class GetBParams(i: Int)
  case class Queryp(getA: GetAParams => Task[A])
  case class A(getB: GetBParams => Task[List[B]])

  implicit lazy val schemaA = Schema.gen[A]

  Schema.gen[Queryp]
}
ghostdogpr commented 4 years ago

Some examples of this pattern within caliban:

I noticed if 2 classes reference each other, it works better if only one of them is made lazy. Magnolia can be a little picky with those.

szymon-rd commented 4 years ago

Actually it gets even worse... I have 4 types, each of them is member of another, and they hold reference to their parent. It forms kind of a chain. Do you know any method to resolve issue like this? I tried a lot of combination with this lazy schema vals, but it doesn't seem to help.

ghostdogpr commented 4 years ago

Hmm, it's hard to help you without the code. Any way you could extract it in something reproducible?

Something like this work for example:

import caliban.schema._
import zio.Task

object Test {
  case class A(getB: GetBParams => Task[List[E]])
  case class B(getA: GetAParams => Task[A])
  case class C(getB: GetAParams => Task[B])
  case class D(getC: GetAParams => Task[C])
  case class E(getC: GetAParams => Task[D])
  case class GetAParams(i: Int)
  case class GetBParams(i: Int)
  case class Queryp(getA: GetAParams => Task[A])

  implicit lazy val schemaA = Schema.gen[A]

  Schema.gen[Queryp]
}
szymon-rd commented 4 years ago

By "chain" I meant a structure like this:

  case class C(parent: Task[B], x: Task[C])
  case class B(parent: Task[A], getC: GetBParams => Task[C])
  case class GetAParams(i: Int)
  case class GetBParams(i: Int)
  case class Queryp(getA: GetAParams => Task[A])
  case class A(getB: GetBParams => Task[List[B]], getC: GetBParams => Task[List[C]])

  implicit lazy val z = Schema.gen[A]
  implicit val y = Schema.gen[Queryp]

And that reproduces the error I have in my case. And thanks for your help :) I kind of got it randomly to work in my case by creating something like implicit lazy Scheme.gen[List[A]], but I can't share my full schema and I just try to reproduce errors I get with different setup. But with the snippet above I just can't find the combination that works. Anyways, even when I get it to work for some reason it introduces a type BList and CList in A type, that are defined like this: union CList = Nil | [C!], with custom scalar Nil (type Nil = { _: Boolean})

ghostdogpr commented 4 years ago

Actually your example works for me, it generates the following shema:

schema {
  query: Queryp
}

type A {
  getB(i: Int!): [B!]
  getC(i: Int!): [C!]
}

type B {
  parent: A
  getC(i: Int!): C
}

type C {
  parent: B
  x: C
}

type Queryp {
  getA(i: Int!): A
}

Do you have the latest version of Caliban? I upgraded Magnolia in 0.7.6 and 0.7.7, maybe it improved things. Otherwise not sure what it is, I tried with 2.12.11 and 2.13.2, both worked 🤔

szymon-rd commented 4 years ago

Thank you for your help, I will try to reproduce it in exact way next week - I'm having problems with replicating the exact behaviour without putting here my company's code :) But I'm using Caliban 0.8.0 on Scala 2.12.

szymon-rd commented 4 years ago

Ok, so after playing with it a bit more I fixed it and I think that the underlying issue was the fact that we were using outdated mercator. Not magnolia, but mercator, as dependency of another library :) Thank you for you help though, I will know what to do when I encounter some actual issues with generating Schema.

ghostdogpr commented 4 years ago

Great news! Glad you got it working.