-
-
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 2248625
Showing
8 changed files
with
302 additions
and
1 deletion.
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
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
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,102 @@ | ||
package caliban.relay | ||
|
||
import caliban.CalibanError.ExecutionError | ||
import caliban.execution.Field | ||
import caliban.introspection.adt.{ __Field, __Type, __TypeKind, TypeVisitor } | ||
import caliban.schema.Step.QueryStep | ||
import caliban.schema.{ ArgBuilder, GenericSchema, Schema, Step } | ||
import caliban.transformers.Transformer | ||
import caliban.{ graphQL, GraphQL, GraphQLAspect, RootResolver } | ||
import zio.NonEmptyChunk | ||
import zio.query.ZQuery | ||
|
||
object RelaySupport { | ||
|
||
def globalIdentifiers[R, ID: ArgBuilder]( | ||
original: GraphQL[R], | ||
resolvers: NonEmptyChunk[NodeResolver[R, ID]] | ||
)(implicit idSchema: Schema[Any, ID], typeResolver: TypeResolver[ID]): GraphQL[R] = { | ||
val _resolvers = resolvers.toList | ||
lazy val _typeMap = _resolvers.flatMap(r => r.toType.name.map(_ -> r)).toMap | ||
|
||
val genericSchema = new GenericSchema[R] {} | ||
implicit val nodeArgBuilder: ArgBuilder[NodeArgs[ID]] = ArgBuilder.gen[NodeArgs[ID]] | ||
implicit val nodeArgsSchema: Schema[Any, NodeArgs[ID]] = Schema.gen[Any, NodeArgs[ID]] | ||
|
||
val nodeType = __Type( | ||
__TypeKind.INTERFACE, | ||
name = Some("Node"), | ||
possibleTypes = Some(_resolvers.map(_.toType)), | ||
fields = _ => | ||
Some( | ||
List( | ||
__Field( | ||
"id", | ||
None, | ||
args = _ => Nil, | ||
`type` = () => idSchema.toType_(isInput = true).nonNull | ||
) | ||
) | ||
) | ||
) | ||
|
||
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 = nodeType | ||
|
||
override def resolve(value: Identifier[ID]): Step[R] = | ||
_typeMap | ||
.get(value.typename) | ||
.fold[Step[R]](Step.NullStep)(resolver => QueryStep(resolver.resolve(value.id))) | ||
} | ||
|
||
val transformer = new Transformer[Any] { | ||
private def shouldAdd(__type: __Type): Boolean = | ||
if (__type.innerType.name.isEmpty) false | ||
else typeNames.contains(__type.name.get) && !__type.implements.contains("Node") | ||
|
||
override val typeVisitor: TypeVisitor = | ||
TypeVisitor.interfaces.addWith(inner => | ||
if (shouldAdd(inner)) List(nodeType) | ||
else Nil | ||
) | ||
|
||
override protected lazy val typeNames: collection.Set[String] = | ||
_typeMap.keySet | ||
|
||
override protected def transformStep[R1 <: Any](step: Step.ObjectStep[R1], field: Field): Step.ObjectStep[R1] = | ||
step | ||
} | ||
|
||
case class Query( | ||
node: NodeArgs[ID] => ZQuery[Any, ExecutionError, Identifier[ID]] | ||
) | ||
|
||
implicit val querySchema: Schema[R, Query] = genericSchema.gen[R, Query] | ||
|
||
(original |+| graphQL[R, Query, Unit, Unit]( | ||
RootResolver( | ||
Query(node = args => ZQuery.fromEither(typeResolver.resolve(args.id)).map(Identifier(_, args.id))) | ||
) | ||
)).transform(transformer) | ||
} | ||
|
||
def withGlobalIdentifiers[ID]: WithGlobalIdentifiers[ID] = | ||
WithGlobalIdentifiers() | ||
|
||
private case class Identifier[A](typename: String, id: A) | ||
|
||
private case class NodeArgs[ID](id: ID) | ||
|
||
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,11 @@ | ||
package caliban.relay | ||
|
||
import caliban.CalibanError.ExecutionError | ||
|
||
/** | ||
* Used in GlobalIds to resolve the type of the returned object based on the ID. | ||
* This is needed in order to correctly derive the subtype of the object. | ||
*/ | ||
trait TypeResolver[ID] { | ||
def resolve(a: ID): Either[ExecutionError, String] | ||
} |
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
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 | ||
|
||
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, String] = | ||
a.value.split(":") match { | ||
case Array(typename, _) => Right(typename) | ||
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[Any, ID] = NodeResolver.from[ID] { id => | ||
ZQuery.succeed(ships.find(s => id.value.endsWith(s.id.value)).map(_.copy(id = id))) | ||
} | ||
|
||
val characterResolver: NodeResolver[Any, ID] = 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 @@ | ||
RelaySupport.withGlobalIdentifiers[ID](shipResolver, characterResolver) | ||
|
||
assertTrue( | ||
DocumentRenderer.renderCompact( | ||
augmented.toDocument | ||
) == "schema{query:Query}interface Node{id:ID!}type Character implements Node{id:ID! name:String!}type Query{characters:[Character!]! ships:[Ship!]! node(id:ID!):Node}type Ship implements Node{id:ID! name:String! purpose:String!}" | ||
) | ||
}, | ||
test("resolve node field") { | ||
val augmented = api @@ | ||
RelaySupport.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 | ||
) | ||
) | ||
) | ||
} | ||
) | ||
|
||
} |