diff --git a/core/src/main/scala/caliban/GraphQL.scala b/core/src/main/scala/caliban/GraphQL.scala index 0736c9275..c8416e88a 100644 --- a/core/src/main/scala/caliban/GraphQL.scala +++ b/core/src/main/scala/caliban/GraphQL.scala @@ -143,9 +143,14 @@ trait GraphQL[-R] { self => } /** - * A symbolic alias for `withWrapper`. + * Attaches an aspect that will wrap the entire GraphQL so that it can be manipulated. + * This method is a higher-level abstraction of [[withWrapper]] which allows the caller to + * completely replace or change all aspects of the schema. + * @param aspect A wrapper type that will be applied to this GraphQL + * @return A new GraphQL API */ - final def @@[R2 <: R](wrapper: Wrapper[R2]): GraphQL[R2] = withWrapper(wrapper) + final def @@[LowerR <: UpperR, UpperR <: R](aspect: GraphQLAspect[LowerR, UpperR]): GraphQL[UpperR] = + aspect(self) /** * Merges this GraphQL API with another GraphQL API. diff --git a/core/src/main/scala/caliban/GraphQLAspect.scala b/core/src/main/scala/caliban/GraphQLAspect.scala new file mode 100644 index 000000000..f46023859 --- /dev/null +++ b/core/src/main/scala/caliban/GraphQLAspect.scala @@ -0,0 +1,18 @@ +package caliban + +/** + * A `GraphQLAspect` is wrapping type similar to a polymorphic function, which is capable + * of transforming a GraphQL into another while possibly enlarging the required environment type. + * It allows a flexible way to augment an existing GraphQL with new capabilities or features. + */ +trait GraphQLAspect[+LowerR, -UpperR] { self => + def apply[R >: LowerR <: UpperR](gql: GraphQL[R]): GraphQL[R] + + def @@[LowerR1 >: LowerR, UpperR1 <: UpperR]( + other: GraphQLAspect[LowerR1, UpperR1] + ): GraphQLAspect[LowerR1, UpperR1] = + new GraphQLAspect[LowerR1, UpperR1] { + def apply[R >: LowerR1 <: UpperR1](gql: GraphQL[R]): GraphQL[R] = + other(self(gql)) + } +} diff --git a/core/src/main/scala/caliban/wrappers/Wrapper.scala b/core/src/main/scala/caliban/wrappers/Wrapper.scala index 15ef81f83..ed2383757 100644 --- a/core/src/main/scala/caliban/wrappers/Wrapper.scala +++ b/core/src/main/scala/caliban/wrappers/Wrapper.scala @@ -1,14 +1,15 @@ package caliban.wrappers -import scala.annotation.tailrec import caliban.CalibanError.{ ExecutionError, ParsingError, ValidationError } import caliban.execution.{ ExecutionRequest, FieldInfo } import caliban.introspection.adt.__Introspection import caliban.parsing.adt.Document import caliban.wrappers.Wrapper.CombinedWrapper -import caliban.{ CalibanError, GraphQLRequest, GraphQLResponse, ResponseValue } -import zio.{ UIO, ZIO } +import caliban._ import zio.query.ZQuery +import zio.{ UIO, ZIO } + +import scala.annotation.tailrec /** * A `Wrapper[-R]` represents an extra layer of computation that can be applied on top of Caliban's query handling. @@ -21,8 +22,11 @@ import zio.query.ZQuery * * It is also possible to combine wrappers using `|+|` and to build a wrapper effectfully with `EffectfulWrapper`. */ -sealed trait Wrapper[-R] { self => +sealed trait Wrapper[-R] extends GraphQLAspect[Nothing, R] { self => def |+|[R1 <: R](that: Wrapper[R1]): Wrapper[R1] = CombinedWrapper(List(self, that)) + + def apply[R1 <: R](that: GraphQL[R1]): GraphQL[R1] = + that.withWrapper(self) } object Wrapper { diff --git a/examples/src/main/scala/example/federation/FederatedApi.scala b/examples/src/main/scala/example/federation/FederatedApi.scala index b33256374..cfcf7acd4 100644 --- a/examples/src/main/scala/example/federation/FederatedApi.scala +++ b/examples/src/main/scala/example/federation/FederatedApi.scala @@ -4,7 +4,7 @@ import example.federation.CharacterService.CharacterService import example.federation.EpisodeService.EpisodeService import caliban.GraphQL.graphQL -import caliban.federation.{EntityResolver, federate} +import caliban.federation.{EntityResolver, federated} import caliban.federation.tracing.ApolloFederatedTracing import caliban.schema.Annotations.{GQLDeprecated, GQLDescription} import caliban.schema.{ArgBuilder, GenericSchema, Schema} @@ -48,19 +48,9 @@ object FederatedApi { implicit val episodeArgs = gen[EpisodeArgs] implicit val episodeArgBuilder: ArgBuilder[EpisodeArgs] = ArgBuilder.gen[EpisodeArgs] - val api: GraphQL[Console with Clock with CharacterService] = - federate( - graphQL( - RootResolver( - Queries( - args => CharacterService.getCharacters(args.origin), - args => CharacterService.findCharacter(args.name) - ), - Mutations(args => CharacterService.deleteCharacter(args.name)) - ) - ) @@ standardWrappers, - EntityResolver.from[CharacterArgs](args => ZQuery.fromEffect(CharacterService.findCharacter(args.name))), - EntityResolver.from[EpisodeArgs](args => + val withFederation = federated( + EntityResolver.from[CharacterArgs](args => ZQuery.fromEffect(CharacterService.findCharacter(args.name))), + EntityResolver.from[EpisodeArgs](args => ZQuery .fromEffect(CharacterService.getCharactersByEpisode(args.season, args.episode)) .map(characters => @@ -72,8 +62,18 @@ object FederatedApi { ) ) ) - ) - ) + )) + + val api: GraphQL[Console with Clock with CharacterService] = + graphQL( + RootResolver( + Queries( + args => CharacterService.getCharacters(args.origin), + args => CharacterService.findCharacter(args.name) + ), + Mutations(args => CharacterService.deleteCharacter(args.name)) + ) + ) @@ standardWrappers @@ withFederation } object Episodes extends GenericSchema[EpisodeService] { @@ -89,7 +89,6 @@ object FederatedApi { implicit val episodeSchema = gen[Episode] val api: GraphQL[Console with Clock with EpisodeService] = - federate( graphQL( RootResolver( Queries( @@ -97,11 +96,12 @@ object FederatedApi { args => EpisodeService.getEpisodes(args.season) ) ) - ) @@ standardWrappers, + ) @@ standardWrappers @@ federated( EntityResolver.from[EpisodeArgs](args => ZQuery.fromEffect(EpisodeService.getEpisode(args.season, args.episode)) ) - ) + ) + } } diff --git a/federation/src/main/scala/caliban/federation/Federation.scala b/federation/src/main/scala/caliban/federation/Federation.scala index 904d1bd65..7111a2485 100644 --- a/federation/src/main/scala/caliban/federation/Federation.scala +++ b/federation/src/main/scala/caliban/federation/Federation.scala @@ -6,7 +6,7 @@ import caliban.introspection.adt._ import caliban.parsing.adt.Directive import caliban.schema.Step.QueryStep import caliban.schema._ -import caliban.{ CalibanError, GraphQL, InputValue, RootResolver } +import caliban.{ CalibanError, GraphQL, GraphQLAspect, InputValue, RootResolver } import zio.query.ZQuery trait Federation { @@ -48,6 +48,18 @@ trait Federation { GraphQL.graphQL(RootResolver(Query(_service = _Service(original.render))), federationDirectives) |+| original } + def federated[R](resolver: EntityResolver[R], others: EntityResolver[R]*): GraphQLAspect[Nothing, R] = + new GraphQLAspect[Nothing, R] { + def apply[R1 <: R](original: GraphQL[R1]): GraphQL[R1] = + federate(original, resolver, others: _*) + } + + lazy val federated: GraphQLAspect[Nothing, Any] = + new GraphQLAspect[Nothing, Any] { + def apply[R1](original: GraphQL[R1]): GraphQL[R1] = + federate(original) + } + /** * Accepts a GraphQL as well as entity resolvers in order to support more advanced federation use cases. This variant * will allow the gateway to query for entities by resolver. diff --git a/federation/src/test/scala/caliban/federation/FederationSpec.scala b/federation/src/test/scala/caliban/federation/FederationSpec.scala index 5d018f6f5..bb682d807 100644 --- a/federation/src/test/scala/caliban/federation/FederationSpec.scala +++ b/federation/src/test/scala/caliban/federation/FederationSpec.scala @@ -52,7 +52,7 @@ object FederationSpec extends DefaultRunnableSpec { override def spec = suite("FederationSpec")( testM("should resolve federated types") { - val interpreter = federate(graphQL(resolver), entityResolver).interpreter + val interpreter = (graphQL(resolver) @@ federated(entityResolver)).interpreter val query = gqldoc(""" query test { @@ -69,7 +69,7 @@ object FederationSpec extends DefaultRunnableSpec { ) }, testM("should not include _entities if not resolvers provided") { - val interpreter = federate(graphQL(resolver)).interpreter + val interpreter = (graphQL(resolver) @@ federated).interpreter val query = gqldoc(""" query test { @@ -93,7 +93,7 @@ object FederationSpec extends DefaultRunnableSpec { ) }, testM("should include orphan entities in sdl") { - val interpreter = federate(graphQL(resolver), orphanResolver).interpreter + val interpreter = (graphQL(resolver) @@ federated(orphanResolver)).interpreter val query = gqldoc("""{ _service { sdl } }""") assertM(interpreter.flatMap(_.execute(query)).map(d => d.data.toString))( @@ -106,7 +106,7 @@ object FederationSpec extends DefaultRunnableSpec { ) }, testM("should include field metadata") { - val interpreter = federate(graphQL(resolver), functionEntityResolver).interpreter + val interpreter = (graphQL(resolver) @@ federated(functionEntityResolver)).interpreter val query = gqldoc(""" query Entities($withNicknames: Boolean = false) { _entities(representations: [{__typename: "Character", name: "Amos Burton"}]) { diff --git a/vuepress/docs/docs/middleware.md b/vuepress/docs/docs/middleware.md index d2bd6432e..1f17b68ee 100644 --- a/vuepress/docs/docs/middleware.md +++ b/vuepress/docs/docs/middleware.md @@ -127,4 +127,31 @@ def withErrorCodeExtensions[R]( case err: ParsingError => err.copy(extensions = Some(ObjectValue(List(("errorCode", StringValue("PARSING_ERROR")))))) } +``` + +## Wrapping the GraphQL + +If you need to implement new functionality that involves not just changes to execution but also to the underlying +schema you can use the higher-level `GraphQLAspect` which allows full control of the resulting `GraphQL` that it wraps. + +Here is such an example that is part of the `federation` package which makes a schema available to be used as a sub-graph in +a federated graph: + +```scala + def federate[R](original: GraphQL[R]): GraphQL[R] = { + import Schema._ + + case class Query( + _service: _Service, + _fieldSet: FieldSet = FieldSet("") + ) + + GraphQL.graphQL(RootResolver(Query(_service = _Service(original.render))), federationDirectives) |+| original + } + + lazy val federated: GraphQLAspect[Nothing, Any] = + new GraphQLAspect[Nothing, Any] { + def apply[R1](original: GraphQL[R1]): GraphQL[R1] = + federate(original) + } ``` \ No newline at end of file