-
-
Notifications
You must be signed in to change notification settings - Fork 251
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f072518
commit f2644c3
Showing
5 changed files
with
260 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package caliban.relay | ||
|
||
trait Node[ID] { | ||
def id: ID | ||
} |
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,32 @@ | ||
package caliban.relay | ||
|
||
import caliban.CalibanError | ||
import caliban.introspection.adt.__Type | ||
import caliban.schema.{ Schema, Step } | ||
import zio.query.ZQuery | ||
|
||
trait NodeResolver[-R, ID] { | ||
def resolve(id: ID): ZQuery[R, CalibanError, Step[R]] | ||
def toType: __Type | ||
} | ||
|
||
object NodeResolver { | ||
|
||
def from[ID]: FromPartiallyApplied[ID] = new FromPartiallyApplied[ID] | ||
|
||
final class FromPartiallyApplied[ID] { | ||
def apply[R, T <: Node[ID]]( | ||
resolver: ID => ZQuery[R, CalibanError, Option[T]] | ||
)(implicit schema: Schema[R, T]): NodeResolver[R, ID] = new NodeResolver[R, ID] { | ||
override def resolve(id: ID): ZQuery[R, CalibanError, Step[R]] = | ||
resolver(id).map(_.fold[Step[R]](Step.NullStep)(schema.resolve)) | ||
|
||
override def toType: __Type = schema.toType_() | ||
} | ||
} | ||
|
||
def apply[R, ID, T <: Node[ID]]( | ||
resolver: ID => ZQuery[R, CalibanError, Option[T]] | ||
)(implicit schema: Schema[R, T]): NodeResolver[R, ID] = | ||
from[ID].apply(resolver) | ||
} |
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,93 @@ | ||
package caliban.relay | ||
|
||
import caliban.CalibanError.ExecutionError | ||
import caliban.introspection.adt.{ __Field, __Type, __TypeKind } | ||
import caliban.relay.RelaySupport.WithGlobalIdentifiers | ||
import caliban.schema.Step.QueryStep | ||
import caliban.schema.{ ArgBuilder, GenericSchema, Schema, Step } | ||
import caliban.{ graphQL, GraphQL, GraphQLAspect, RootResolver } | ||
import zio.NonEmptyChunk | ||
import zio.query.ZQuery | ||
|
||
trait TypeResolver[A] { | ||
def resolve(a: A): Either[ExecutionError, Identifier[A]] | ||
} | ||
|
||
case class Identifier[A](typename: String, id: A) | ||
|
||
abstract class RelaySupport { | ||
|
||
def globalIdentifiers[R, ID: ArgBuilder]( | ||
original: GraphQL[R], | ||
resolvers: NonEmptyChunk[NodeResolver[R, ID]] | ||
)(implicit idSchema: Schema[Any, ID], identifiable: TypeResolver[ID]): GraphQL[R] = { | ||
val _resolvers = resolvers.toList | ||
val genericSchema = new GenericSchema[R] {} | ||
|
||
case class NodeArgs(id: ID) | ||
|
||
implicit val nodeArgBuilder: ArgBuilder[NodeArgs] = | ||
ArgBuilder.gen[NodeArgs] | ||
implicit val nodeArgsSchema: Schema[Any, NodeArgs] = Schema.gen | ||
|
||
implicit val nodeSchema: Schema[R, Identifier[ID]] = new Schema[R, Identifier[ID]] { | ||
override val nullable: Boolean = true | ||
override def toType(isInput: Boolean, isSubscription: Boolean): __Type = | ||
__Type( | ||
__TypeKind.INTERFACE, | ||
name = Some("Node"), | ||
possibleTypes = Some(_resolvers.map(_.toType)), | ||
fields = _ => | ||
Some( | ||
List( | ||
__Field( | ||
"id", | ||
None, | ||
args = _ => Nil, | ||
`type` = () => idSchema.toType_(isInput, isSubscription).nonNull | ||
) | ||
) | ||
) | ||
) | ||
|
||
private lazy val _nodeMap = _resolvers.flatMap(r => r.toType.name.map(_ -> r)).toMap | ||
|
||
override def resolve(value: Identifier[ID]): Step[R] = | ||
_nodeMap | ||
.get(value.typename) | ||
.fold[Step[R]](Step.NullStep)(resolver => QueryStep(resolver.resolve(value.id))) | ||
} | ||
|
||
case class Query( | ||
node: NodeArgs => ZQuery[Any, ExecutionError, Identifier[ID]] | ||
) | ||
|
||
implicit val querySchema: Schema[R, Query] = genericSchema.gen[R, Query] | ||
|
||
graphQL[R, Query, Unit, Unit]( | ||
RootResolver( | ||
Query(node = args => ZQuery.fromEither(identifiable.resolve(args.id))) | ||
) | ||
) |+| original | ||
} | ||
|
||
def withGlobalIdentifiers[ID]: WithGlobalIdentifiers[ID] = | ||
WithGlobalIdentifiers() | ||
|
||
} | ||
|
||
object RelaySupport { | ||
case class ID(typename: String, id: String) | ||
|
||
case class WithGlobalIdentifiers[ID](dummy: Boolean = false) extends AnyVal { | ||
def apply[R](resolver: NodeResolver[R, ID], rest: NodeResolver[R, ID]*)(implicit | ||
argBuilder: ArgBuilder[ID], | ||
schema: Schema[Any, ID], | ||
identifiable: TypeResolver[ID] | ||
): GraphQLAspect[Nothing, R] = | ||
new GraphQLAspect[Nothing, R] { | ||
def apply[R1 <: R](original: GraphQL[R1]): GraphQL[R1] = | ||
globalIdentifiers[R1, ID](original, NonEmptyChunk(resolver, rest: _*)) | ||
} | ||
} | ||
} |
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,3 @@ | ||
package caliban | ||
|
||
package object relay extends RelaySupport |
127 changes: 127 additions & 0 deletions
127
core/src/test/scala/caliban/relay/GlobalIdentifierSpec.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,127 @@ | ||
package caliban.relay | ||
|
||
import caliban.CalibanError.ExecutionError | ||
import caliban.Value.StringValue | ||
import caliban.rendering.DocumentRenderer | ||
import caliban.{ graphQL, CalibanError, GraphQLResponse, InputValue, ResponseValue, RootResolver, Value } | ||
import zio.test.{ assertTrue, assertZIO, ZIOSpecDefault } | ||
import caliban.schema.Schema.auto._ | ||
import caliban.schema.ArgBuilder.auto._ | ||
import caliban.schema.{ ArgBuilder, Schema } | ||
import zio.query.ZQuery | ||
import zio.test.Assertion.{ equalTo, isRight } | ||
|
||
object GlobalIdentifierSpec extends ZIOSpecDefault { | ||
case class ID(value: String) | ||
|
||
implicit val schema: caliban.schema.Schema[Any, ID] = Schema.scalarSchema( | ||
"ID", | ||
None, | ||
None, | ||
None, | ||
id => StringValue(id.value) | ||
) | ||
|
||
implicit val argBuilder: caliban.schema.ArgBuilder[ID] = | ||
new ArgBuilder[ID] { | ||
override def build(input: InputValue): Either[ExecutionError, ID] = | ||
input match { | ||
case StringValue(value) => Right(ID(value)) | ||
case _ => Left(ExecutionError("Expected a string")) | ||
} | ||
} | ||
|
||
implicit val typeResolver: TypeResolver[ID] = new TypeResolver[ID] { | ||
override def resolve(a: ID): Either[ExecutionError, Identifier[ID]] = | ||
a.value.split(":") match { | ||
case Array(typename, _) => Right(Identifier(typename, a)) | ||
case _ => Left(ExecutionError("Invalid id")) | ||
} | ||
} | ||
|
||
case class Ship(id: ID, name: String, purpose: String) extends Node[ID] | ||
case class Character(id: ID, name: String) extends Node[ID] | ||
case class Query( | ||
characters: List[Character], | ||
ships: List[Ship] | ||
) | ||
|
||
val characters = List( | ||
Character(ID("1"), "James Holden"), | ||
Character(ID("2"), "Naomi Nagata"), | ||
Character(ID("3"), "Amos Burton"), | ||
Character(ID("4"), "Alex Kamal") | ||
) | ||
|
||
val ships = List( | ||
Ship(ID("1"), "Rocinante", "Stealth Frigate"), | ||
Ship(ID("2"), "Canterbury", "Destroyer"), | ||
Ship(ID("3"), "Nauvoo", "Generation Ship"), | ||
Ship(ID("4"), "Behemoth", "Belter Ship") | ||
) | ||
|
||
val shipResolver = NodeResolver.from[ID] { id => | ||
ZQuery.succeed(ships.find(s => id.value.endsWith(s.id.value)).map(_.copy(id = id))) | ||
} | ||
|
||
val characterResolver = NodeResolver.from[ID] { id => | ||
ZQuery.succeed(characters.find(c => id.value.endsWith(c.id.value)).map(_.copy(id = id))) | ||
} | ||
|
||
val api = graphQL( | ||
RootResolver( | ||
Query( | ||
characters = characters, | ||
ships = ships | ||
) | ||
) | ||
) | ||
|
||
val spec = suite("GlobalIdentifierSpec")( | ||
test("augment schema with node field") { | ||
val augmented = api @@ | ||
withGlobalIdentifiers[ID](shipResolver, characterResolver) | ||
|
||
assertTrue( | ||
DocumentRenderer.renderCompact( | ||
augmented.toDocument | ||
) == "schema{query:Query}interface Node{id:ID!}type Character{id:ID! name:String!}type Query{node(id:ID!):Node characters:[Character!]! ships:[Ship!]!}type Ship{id:ID! name:String! purpose:String!}" | ||
) | ||
}, | ||
test("resolve node field") { | ||
val augmented = api @@ | ||
withGlobalIdentifiers[ID](shipResolver, characterResolver) | ||
|
||
val query = | ||
"""{ | ||
| character: node(id:"Character:2"){ | ||
| id,...on Character{name} | ||
| } | ||
| ship: node(id:"Ship:3"){ | ||
| id,...on Ship{purpose} | ||
| } | ||
|}""".stripMargin | ||
|
||
val result = augmented.interpreterUnsafe.execute(query) | ||
|
||
assertZIO(result)( | ||
equalTo( | ||
GraphQLResponse[CalibanError]( | ||
data = ResponseValue.ObjectValue( | ||
List( | ||
"character" -> ResponseValue.ObjectValue( | ||
List("id" -> Value.StringValue("Character:2"), "name" -> Value.StringValue("Naomi Nagata")) | ||
), | ||
"ship" -> ResponseValue.ObjectValue( | ||
List("id" -> Value.StringValue("Ship:3"), "purpose" -> Value.StringValue("Generation Ship")) | ||
) | ||
) | ||
), | ||
errors = Nil | ||
) | ||
) | ||
) | ||
} | ||
) | ||
|
||
} |