Skip to content

Commit

Permalink
Merge pull request #35 from getquill/example-using-caliban-zhttp
Browse files Browse the repository at this point in the history
Improving Flicers and integration of Caliban
  • Loading branch information
deusaquilus authored Oct 31, 2021
2 parents a71f685 + 2ec84c6 commit 0a64af4
Show file tree
Hide file tree
Showing 26 changed files with 848 additions and 323 deletions.
29 changes: 26 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ lazy val `quill-sql` =
Seq()
else
Seq(
"org.scalatest" % "scalatest_3" % "3.2.9" % "test",
"org.scalatest" % "scalatest-mustmatchers_3" % "3.2.9" % "test",
"org.scalatest" %% "scalatest" % "3.2.9" % Test,
"org.scalatest" %% "scalatest-mustmatchers" % "3.2.9" % Test,
"com.vladsch.flexmark" % "flexmark-all" % "0.35.10" % Test
)
},
Expand Down Expand Up @@ -198,6 +198,27 @@ lazy val `quill-jasync-postgres` =
)
.dependsOn(`quill-jasync` % "compile->compile;test->test")

lazy val `quill-caliban` =
(project in file("quill-caliban"))
.settings(commonSettings: _*)
.settings(releaseSettings: _*)
.settings(
Test / fork := true,
libraryDependencies ++= Seq(
"com.github.ghostdogpr" %% "caliban" % "1.2.0",
"com.github.ghostdogpr" %% "caliban-zio-http" % "1.2.0",
// Adding this to main dependencies would force users to use logback-classic for SLF4j unless the specifically remove it
// seems to be safer to just exclude & add a commented about need for a SLF4j implementation in Docs.
"ch.qos.logback" % "logback-classic" % "1.2.3" % Test,
"io.d11" %% "zhttp" % "1.0.0.0-RC17" % Test,
// Don't want to make this dependant on zio-test for the testing code so importing this here separately
"org.scalatest" %% "scalatest" % "3.2.9" % Test,
"org.scalatest" %% "scalatest-mustmatchers" % "3.2.9" % Test,
"org.postgresql" % "postgresql" % "42.2.18" % Test,
)
)
.dependsOn(`quill-jdbc-zio` % "compile->compile")

lazy val `quill-zio` =
(project in file("quill-zio"))
.settings(commonSettings: _*)
Expand All @@ -217,6 +238,8 @@ lazy val `quill-jdbc-zio` =
.settings(releaseSettings: _*)
.settings(jdbcTestingLibraries: _*)
.settings(
Test / runMain / fork := true,
Test / fork := true,
Test / testGrouping := {
(Test / definedTests).value map { test =>
if (test.name endsWith "IntegrationSpec")
Expand Down Expand Up @@ -284,7 +307,7 @@ lazy val jdbcTestingSettings = jdbcTestingLibraries ++ Seq(

lazy val basicSettings = Seq(
scalaVersion := {
if (isCommunityBuild) dottyLatestNightlyBuild.get else "3.0.0"
if (isCommunityBuild) dottyLatestNightlyBuild.get else "3.0.2"
},
organization := "io.getquill",
// The -e option is the 'error' report of ScalaTest. We want it to only make a log
Expand Down
53 changes: 53 additions & 0 deletions quill-caliban/src/main/scala/io/getquill/CalibanIntegration.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.getquill

import caliban.CalibanError.ExecutionError
import caliban.GraphQL.graphQL
import caliban.introspection.adt.{ __InputValue, __Type, __TypeKind }
import caliban.schema.{ ArgBuilder, Schema, Step }
import caliban.{ InputValue, RootResolver }

case class ProductArgs[T](keyValues: Map[String, String])

object CalibanIntegration:

def flattenToPairs(key: String, value: InputValue): List[(String, String)] =
value match {
// If it contains other keys, continue to get the pairs inside
// e.g. for `name` in Person(name: Name, age: Int) this would be Name from which we need first:String, last:String
case InputValue.ObjectValue(fields) => fields.toList.flatMap { case (k, v) => flattenToPairs(k, v) }
case _ => List((key, value.toInputString))
}

implicit def productArgBuilder[T]: ArgBuilder[ProductArgs[T]] = {
case InputValue.ObjectValue(fields) =>
Right(ProductArgs[T](fields.flatMap { case (k, v) => flattenToPairs(k, v).toMap }))
case other => Left(ExecutionError(s"Can't build a ProductArgs from input $other"))
}

implicit def productSchema[T](implicit ev: Schema[Any, T]): Schema[Any, ProductArgs[T]] =
new Schema[Any, ProductArgs[T]] {

def makeOptionalRecurse(f: __InputValue): __InputValue = {
val fieldType = f.`type`()
val optionalFieldType = fieldType.kind match {
case __TypeKind.NON_NULL => fieldType.ofType.getOrElse(fieldType)
case _ => fieldType
}
f.copy(`type` =
() => optionalFieldType.copy(inputFields = optionalFieldType.inputFields.map(_.map(makeOptionalRecurse)))
)
}

protected[this] def toType(isInput: Boolean, isSubscription: Boolean): __Type =
__Type(
__TypeKind.INPUT_OBJECT,
inputFields = ev
.toType_(isInput, isSubscription)
.inputFields
.map(_.map(f => makeOptionalRecurse(f)))
)

def resolve(value: ProductArgs[T]): Step[Any] = Step.NullStep
}

end CalibanIntegration
6 changes: 6 additions & 0 deletions quill-caliban/src/test/resources/application.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
testPostgresDB.dataSourceClassName=org.postgresql.ds.PGSimpleDataSource
testPostgresDB.dataSource.user=postgres
testPostgresDB.dataSource.password=${?POSTGRES_PASSWORD}
testPostgresDB.dataSource.databaseName=quill_test
testPostgresDB.dataSource.portNumber=${?POSTGRES_PORT}
testPostgresDB.dataSource.serverName=${?POSTGRES_HOST}
99 changes: 99 additions & 0 deletions quill-caliban/src/test/scala/io/getquill/CalibanExample.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package io.getquill.context.cassandra.zio.examples

import caliban.GraphQL.graphQL
import caliban.schema.Annotations.GQLDescription
import caliban.{RootResolver, ZHttpAdapter}
import zhttp.http._
import zhttp.service.Server
import zio.{ExitCode, ZEnv, ZIO}
import io.getquill._
import io.getquill.context.qzio.ImplicitSyntax._
import io.getquill.context.ZioJdbc._
import io.getquill.util.LoadConfig
import zio.console.putStrLn
import zio.{ App, ExitCode, Has, URIO, Task }
import java.io.Closeable
import javax.sql.DataSource

import scala.language.postfixOps
import caliban.execution.Field
import caliban.schema.ArgBuilder
import io.getquill.CalibanIntegration._

case class PersonT(id: Int, name: String, age: Int)
case class AddressT(ownerId: Int, street: String)
case class PersonAddress(id: Int, name: String, age: Int, street: Option[String])

case class PersonAddressPlanQuery(plan: String, pa: List[PersonAddress])

object Dao:

object Ctx extends PostgresZioJdbcContext(Literal)
import Ctx._
lazy val ds = JdbcContextConfig(LoadConfig("testPostgresDB")).dataSource
given Implicit[Has[DataSource with Closeable]] = Implicit(Has(ds))

inline def q(inline columns: List[String], inline filters: Map[String, String]) =
quote {
query[PersonT].leftJoin(query[AddressT]).on((p, a) => p.id == a.ownerId)
.map((p, a) => PersonAddress(p.id, p.name, p.age, a.map(_.street)))
.filterColumns(columns)
.filterByKeys(filters)
.take(10)
}
inline def plan(inline columns: List[String], inline filters: Map[String, String]) =
quote { infix"EXPLAIN ${q(columns, filters)}".pure.as[Query[String]] }

def personAddress(columns: List[String], filters: Map[String, String]) =
run(q(columns, filters)).implicitDS.mapError(e => {
println("===========ERROR===========" + e.getMessage) //
e
})

def personAddressPlan(columns: List[String], filters: Map[String, String]) =
run(plan(columns, filters), OuterSelectWrap.Never).map(_.mkString("\n")).implicitDS.mapError(e => {
println("===========ERROR===========" + e.getMessage) //helloooo
e
})
end Dao

case class Queries(
personAddress: Field => (ProductArgs[PersonAddress] => Task[List[PersonAddress]]),
personAddressPlan: Field => (ProductArgs[PersonAddress] => Task[PersonAddressPlanQuery])
)
object CalibanExample extends zio.App {

val myApp = for {
interpreter <- graphQL(
RootResolver(
Queries(
personAddress =>
(productArgs =>
Dao.personAddress(personAddress.fields.map(_.name), productArgs.keyValues)
),
personAddressPlan =>
(productArgs => {
val cols = personAddressPlan.fields.flatMap(_.fields.map(_.name))
println(s"==== Selected Columns: ${cols}")
//println(s"==== Nested Fields: ${personAddressPlan.fields.map(_.fields.map(_.name))}")
(Dao.personAddressPlan(cols, productArgs.keyValues) zip Dao.personAddress(cols, productArgs.keyValues)).map(
(pa, plan) => PersonAddressPlanQuery(pa, plan)
)
})
)
)
).interpreter
_ <- Server
.start(
port = 8088,
http = Http.route { case _ -> Root / "api" / "graphql" =>
ZHttpAdapter.makeHttpService(interpreter)
}
)
.forever
} yield ()

override def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] =
myApp.exitCode

}
133 changes: 133 additions & 0 deletions quill-caliban/src/test/scala/io/getquill/CalibanIntegrationSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package io.getquill

import org.scalatest.BeforeAndAfterAll
import org.scalatest.freespec.AnyFreeSpec
import org.scalatest.matchers.must.Matchers

import io.getquill.context.ZioJdbc.DataSourceLayer
import zio.{ZIO, Task}
import io.getquill.context.ZioJdbc._
import caliban.execution.Field
import caliban.schema.ArgBuilder
import caliban.GraphQL.graphQL
import caliban.schema.Annotations.GQLDescription
import caliban.RootResolver
import io.getquill.CalibanIntegration._

class CalibanIntegrationSpec extends AnyFreeSpec with Matchers with BeforeAndAfterAll {
object Ctx extends PostgresZioJdbcContext(Literal)
import Ctx._
lazy val zioDS = DataSourceLayer.fromPrefix("testPostgresDB")

extension [R, E, A](qzio: ZIO[Any, Throwable, A])
def unsafeRunSync(): A = zio.Runtime.default.unsafeRun(qzio)

override def beforeAll() = {
import Flat._
(for {
_ <- Ctx.run(infix"TRUNCATE TABLE AddressT, PersonT RESTART IDENTITY".as[Delete[PersonT]])
_ <- Ctx.run(liftQuery(List(
PersonT(1, "One", "A", 44),
PersonT(2, "Two", "B", 55),
PersonT(3, "Three", "C", 66)
)).foreach(row => query[PersonT].insert(row)))
_ <- Ctx.run(liftQuery(List(
AddressT(1, "123 St"),
AddressT(2, "789 St")
)).foreach(row => query[AddressT].insert(row)))
} yield ()
).onDataSource.provideLayer(zioDS).unsafeRunSync()
}

override def afterAll() = {
import Flat._
Ctx.run(infix"TRUNCATE TABLE AddressT, PersonT RESTART IDENTITY".as[Delete[PersonT]]).onDataSource.provideLayer(zioDS).unsafeRunSync()
}

object Flat:
case class PersonT(id: Int, first: String, last: String, age: Int)
case class AddressT(ownerId: Int, street: String)
case class PersonAddressFlat(id: Int, first: String, last: String, age: Int, street: Option[String])
object Dao:
def personAddress(columns: List[String], filters: Map[String, String]) =
Ctx.run {
query[PersonT].leftJoin(query[AddressT]).on((p, a) => p.id == a.ownerId)
.map((p, a) => PersonAddressFlat(p.id, p.first, p.last, p.age, a.map(_.street)))
.filterByKeys(filters)
.filterColumns(columns)
.take(10)
}.onDataSource.provideLayer(zioDS)

object Nested:
case class Name(first: String, last: String)
case class PersonT(id: Int, name: Name, age: Int)
case class AddressT(ownerId: Int, street: String)
// Needs to be named differently from Flat.PersonAddress___ since Caliban infers from this class & name must be different
case class PersonAddressNested(id: Int, name: Name, age: Int, street: Option[String])
object Dao:
def personAddress(columns: List[String], filters: Map[String, String]) =
Ctx.run {
query[PersonT].leftJoin(query[AddressT]).on((p, a) => p.id == a.ownerId)
.map((p, a) => PersonAddressNested(p.id, p.name, p.age, a.map(_.street)))
.filterByKeys(filters)
.filterColumns(columns)
.take(10)
}.onDataSource.provideLayer(zioDS)

case class Queries(
personAddressFlat: Field => (ProductArgs[Flat.PersonAddressFlat] => Task[List[Flat.PersonAddressFlat]]),
personAddressJoined: Field => (ProductArgs[Nested.PersonAddressNested] => Task[List[Nested.PersonAddressNested]])
)

val api = graphQL(
RootResolver(
Queries(
personAddressFlat =>
(productArgs =>
Flat.Dao.personAddress(personAddressFlat.fields.map(_.name), productArgs.keyValues)
),
personAddressNested =>
(productArgs =>
Nested.Dao.personAddress(personAddressNested.fields.map(_.name), productArgs.keyValues)
),
)
)
)

def unsafeRunQuery(queryString: String) =
(for {
interpreter <- api.interpreter
result <- interpreter.execute(queryString)
} yield (result)).unsafeRunSync()

object CalibanQueries:
// Purposely filtering by a column that is not in the selection to test this case
val flatWithJoin =
"""
{
personAddressFlat(first: One) {
id
first
last
street
}
}"""
val flatWithJoinColNotIncluded =
"""
{
personAddressFlat(first: One) {
id
last
street
}
}"""

"Caliban integration should work for" - {
"flat object with filteration column included" in {
unsafeRunQuery(CalibanQueries.flatWithJoin).data.toString mustEqual """{"personAddressFlat":[{"id":1,"first":"One","last":"A","street":"123 St"}]}"""
}
"flat object with filteration column excluded" in {
unsafeRunQuery(CalibanQueries.flatWithJoinColNotIncluded).data.toString mustEqual """{"personAddressFlat":[{"id":1,"last":"A","street":"123 St"}]}"""
}
}
}
Loading

0 comments on commit 0a64af4

Please sign in to comment.