Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add federation 2.8, fix federation v2 introspection #2273

Merged
merged 2 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions core/src/main/scala/caliban/Value.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import caliban.interop.zio.{ IsZIOJsonDecoder, IsZIOJsonEncoder }
import caliban.rendering.ValueRenderer
import zio.stream.Stream

import scala.annotation.tailrec
import scala.util.control.NonFatal
import scala.util.hashing.MurmurHash3

Expand Down Expand Up @@ -67,6 +68,38 @@ sealed trait ResponseValue extends Serializable { self =>
}
}
object ResponseValue {

def at(path: List[PathValue])(value: ResponseValue): ResponseValue = {
def loop(path: List[PathValue], value: ResponseValue): ResponseValue = path match {
case Nil => value
case PathValue.Key(key) :: tail =>
value match {
case ObjectValue(fields) =>
fields.find(_._1 == key) match {
case Some((_, v)) => loop(tail, v)
case None => Value.NullValue
}
case ListValue(values) =>
ListValue(values.map(loop(path, _)))
case _ => Value.NullValue
}
case PathValue.Index(index) :: tail =>
value match {
case ListValue(values) =>
val idx = index
if (idx < values.size) {
loop(tail, values(idx))
} else {
Value.NullValue
}
case _ => Value.NullValue
}
case _ => Value.NullValue
}

loop(path, value)
}

case class ListValue(values: List[ResponseValue]) extends ResponseValue {
override def toString: String = ValueRenderer.responseListValueRenderer.renderCompact(this)
}
Expand Down
4 changes: 2 additions & 2 deletions examples/src/main/resources/gateway_v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"keywords": [],
"license": "MIT",
"dependencies": {
"@apollo/server": "4.9.5",
"@apollo/gateway": "2.6.2",
"@apollo/gateway": "2.8.0",
"@apollo/server": "4.10.4",
"graphql": "16.8.1"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,29 @@ abstract class FederationSupport(
) {
import FederationHelpers._

// This is a bit of a hack to determine if we are using the v1 version of the federation spec
// All of the v2 directives come through schema directives while the v1 is through the supported directives field instead
private val isV1 = supportedDirectives.nonEmpty && schemaDirectives.isEmpty
private val extraTypes = if (isV1) List(fieldSetSchema.toType_()) else Nil

/**
* Accepts a GraphQL and returns a GraphQL with the minimum settings to support federation. This variant does not
* provide any stitching capabilities, it merely makes this schema consumable by a graphql federation gateway.
* @param original The original schema
* @return A new schema which has been augmented with federation types
*/
def federate[R](original: GraphQL[R]): GraphQL[R] = {
import caliban.schema.Schema.auto._

case class Query(
_service: _Service,
_fieldSet: FieldSet = FieldSet("")
_service: _Service
)

implicit val serviceSchema = Schema.gen[R, _Service]
implicit val querySchema = Schema.gen[R, Query]

graphQL(
RootResolver(Query(_service = _Service(original.withSchemaDirectives(schemaDirectives).render))),
supportedDirectives
) |+| original
).withAdditionalTypes(extraTypes) |+| original
}

def federated[R](resolver: EntityResolver[R], others: EntityResolver[R]*): GraphQLAspect[Nothing, R] =
Expand All @@ -57,7 +62,6 @@ abstract class FederationSupport(
val resolvers = resolver +: otherResolvers.toList

val genericSchema = new GenericSchema[R] {}
import genericSchema.auto._

implicit val entitySchema: Schema[R, _Entity] = new Schema[R, _Entity] {
override def nullable: Boolean = true
Expand All @@ -84,14 +88,17 @@ abstract class FederationSupport(

case class Query(
_entities: RepresentationsArgs => List[_Entity],
_service: ZQuery[Any, Nothing, _Service],
_fieldSet: FieldSet = FieldSet("")
_service: ZQuery[Any, Nothing, _Service]
)

val withSDL = original
.withAdditionalTypes(resolvers.map(_.toType).flatMap(Types.collectTypes(_)))
.withSchemaDirectives(schemaDirectives)

implicit val representationsArgsSchema: Schema[Any, RepresentationsArgs] = Schema.gen
implicit val serviceSchema: Schema[R, _Service] = genericSchema.gen[R, _Service]
implicit val querySchema: Schema[R, Query] = genericSchema.gen[R, Query]

graphQL[R, Query, Unit, Unit](
RootResolver(
Query(
Expand All @@ -100,7 +107,7 @@ abstract class FederationSupport(
)
),
supportedDirectives
) |+| original
).withAdditionalTypes(extraTypes) |+| original

}
}
3 changes: 3 additions & 0 deletions federation/src/main/scala/caliban/federation/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import caliban.federation.v2x.{
FederationDirectivesV2_3,
FederationDirectivesV2_5,
FederationDirectivesV2_6,
FederationDirectivesV2_8,
FederationV2,
Versions
}
Expand All @@ -17,5 +18,7 @@ package object federation {
lazy val v2_4 = new FederationV2(List(Versions.v2_4)) with FederationDirectivesV2_3
lazy val v2_5 = new FederationV2(List(Versions.v2_5)) with FederationDirectivesV2_5
lazy val v2_6 = new FederationV2(List(Versions.v2_6)) with FederationDirectivesV2_6
lazy val v2_7 = new FederationV2(List(Versions.v2_7)) with FederationDirectivesV2_6
lazy val v2_8 = new FederationV2(List(Versions.v2_8)) with FederationDirectivesV2_8

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package caliban.federation.v2x

import caliban.Value.StringValue
import caliban.parsing.adt.Directive
import caliban.schema.Annotations.GQLDirective

trait FederationDirectivesV2_8 extends FederationDirectivesV2_6 {

def Context(context: String) = Directive("context", Map("context" -> StringValue(context)))

case class GQLContext(context: String) extends GQLDirective(Context(context))

def FromContext(context: String) = Directive("fromContext", Map("context" -> StringValue(context)))

case class GQLFromContext(context: String) extends GQLDirective(FromContext(context))
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,14 @@ object FederationV2 {
`import` = v2_5.`import` :+ Import("@policy")
)

private[v2x] val v2_7 = Link(
url = s"$federationV2Url/v2.7",
`import` = v2_6.`import`
)

private[v2x] val v2_8 = Link(
url = s"$federationV2Url/v2.8",
`import` = v2_7.`import` :+ Import("@context") :+ Import("@fromContext")
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ object Versions {
val v2_4 = FederationV2.v2_4
val v2_5 = FederationV2.v2_5
val v2_6 = FederationV2.v2_6
val v2_7 = FederationV2.v2_7
val v2_8 = FederationV2.v2_8

}
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,30 @@ object FederationV1Spec extends ZIOSpecDefault {
)
)
}
},
test("introspection should include _Any and _FieldSet scalars") {
val interpreter = (graphQL(resolver) @@ federated(entityResolver)).interpreter

val query = gqldoc("""{ __schema { types { name } } }""")
interpreter
.flatMap(_.execute(query))
.map(d =>
ResponseValue.at(
PathValue.Key("__schema") :: PathValue.Key("types") :: PathValue.Key("name") :: Nil
)(d.data)
)
.map(responseValue =>
assertTrue(
responseValue
.is(_.subtype[ListValue])
.values
.contains(StringValue("_Any")),
responseValue
.is(_.subtype[ListValue])
.values
.contains(StringValue("_FieldSet"))
)
)
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package caliban.federation.v2x

import caliban.InputValue.{ ListValue, ObjectValue }
import caliban.Macros.gqldoc
import caliban.TestUtils._
import caliban.Value.StringValue
import caliban.parsing.Parser
import caliban.parsing.adt.{ Definition, Directive }
Expand All @@ -11,7 +12,6 @@ import io.circe.Json
import io.circe.parser.decode
import zio.ZIO
import zio.test.Assertion.{ hasSameElements, isSome }
import zio.test.{ assertTrue, ZIOSpecDefault }
import zio.test._

object FederationV2Spec extends ZIOSpecDefault {
Expand Down Expand Up @@ -182,6 +182,30 @@ object FederationV2Spec extends ZIOSpecDefault {
)
)
}
},
test("introspection doesn't contain _FieldSet scalar") {
import caliban.federation.v2_8._
val interpreter = (graphQL(resolver) @@ federated).interpreter
val query = gqldoc("""{ __schema { types { name } } }""")
interpreter
.flatMap(_.execute(query))
.map(d =>
ResponseValue.at(
PathValue.Key("__schema") :: PathValue.Key("types") :: PathValue.Key("name") :: Nil
)(d.data)
)
.map(responseValue =>
assertTrue(
!responseValue
.is(_.subtype[ResponseValue.ListValue])
.values
.contains(StringValue("_Any")),
!responseValue
.is(_.subtype[ResponseValue.ListValue])
.values
.contains(StringValue("_FieldSet"))
)
)
}
)

Expand Down
Loading