-
Notifications
You must be signed in to change notification settings - Fork 348
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #35 from getquill/example-using-caliban-zhttp
Improving Flicers and integration of Caliban
- Loading branch information
Showing
26 changed files
with
848 additions
and
323 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
53 changes: 53 additions & 0 deletions
53
quill-caliban/src/main/scala/io/getquill/CalibanIntegration.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
99
quill-caliban/src/test/scala/io/getquill/CalibanExample.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
133
quill-caliban/src/test/scala/io/getquill/CalibanIntegrationSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"}]}""" | ||
} | ||
} | ||
} |
Oops, something went wrong.