diff --git a/core/src/main/scala/caliban/Value.scala b/core/src/main/scala/caliban/Value.scala index f0190db279..ba17b1d910 100644 --- a/core/src/main/scala/caliban/Value.scala +++ b/core/src/main/scala/caliban/Value.scala @@ -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 @@ -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) } diff --git a/examples/src/main/resources/gateway_v2/package.json b/examples/src/main/resources/gateway_v2/package.json index df5a301aa2..2920c27889 100644 --- a/examples/src/main/resources/gateway_v2/package.json +++ b/examples/src/main/resources/gateway_v2/package.json @@ -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" } } diff --git a/federation/src/main/scala/caliban/federation/FederationSupport.scala b/federation/src/main/scala/caliban/federation/FederationSupport.scala index e4d695f94c..c785998115 100644 --- a/federation/src/main/scala/caliban/federation/FederationSupport.scala +++ b/federation/src/main/scala/caliban/federation/FederationSupport.scala @@ -13,6 +13,11 @@ 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. @@ -20,17 +25,17 @@ abstract class FederationSupport( * @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] = @@ -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 @@ -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( @@ -100,7 +107,7 @@ abstract class FederationSupport( ) ), supportedDirectives - ) |+| original + ).withAdditionalTypes(extraTypes) |+| original } } diff --git a/federation/src/main/scala/caliban/federation/package.scala b/federation/src/main/scala/caliban/federation/package.scala index d7405ad79c..59a3ab4def 100644 --- a/federation/src/main/scala/caliban/federation/package.scala +++ b/federation/src/main/scala/caliban/federation/package.scala @@ -4,6 +4,7 @@ import caliban.federation.v2x.{ FederationDirectivesV2_3, FederationDirectivesV2_5, FederationDirectivesV2_6, + FederationDirectivesV2_8, FederationV2, Versions } @@ -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 } diff --git a/federation/src/main/scala/caliban/federation/v2x/FederationDirectivesV2_8.scala b/federation/src/main/scala/caliban/federation/v2x/FederationDirectivesV2_8.scala new file mode 100644 index 0000000000..129de86672 --- /dev/null +++ b/federation/src/main/scala/caliban/federation/v2x/FederationDirectivesV2_8.scala @@ -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)) +} diff --git a/federation/src/main/scala/caliban/federation/v2x/FederationV2.scala b/federation/src/main/scala/caliban/federation/v2x/FederationV2.scala index 8716756b79..e6f6a782f3 100644 --- a/federation/src/main/scala/caliban/federation/v2x/FederationV2.scala +++ b/federation/src/main/scala/caliban/federation/v2x/FederationV2.scala @@ -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") + ) + } diff --git a/federation/src/main/scala/caliban/federation/v2x/Versions.scala b/federation/src/main/scala/caliban/federation/v2x/Versions.scala index 4e01494b35..9e464f8a5a 100644 --- a/federation/src/main/scala/caliban/federation/v2x/Versions.scala +++ b/federation/src/main/scala/caliban/federation/v2x/Versions.scala @@ -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 } diff --git a/federation/src/test/scala/caliban/federation/FederationV1Spec.scala b/federation/src/test/scala/caliban/federation/FederationV1Spec.scala index 7a160ea1fd..995a070aef 100644 --- a/federation/src/test/scala/caliban/federation/FederationV1Spec.scala +++ b/federation/src/test/scala/caliban/federation/FederationV1Spec.scala @@ -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")) + ) + ) } ) } diff --git a/federation/src/test/scala/caliban/federation/v2x/FederationV2Spec.scala b/federation/src/test/scala/caliban/federation/v2x/FederationV2Spec.scala index b51fe35b12..b6b1c46d82 100644 --- a/federation/src/test/scala/caliban/federation/v2x/FederationV2Spec.scala +++ b/federation/src/test/scala/caliban/federation/v2x/FederationV2Spec.scala @@ -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 } @@ -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 { @@ -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")) + ) + ) } )