diff --git a/modules/core/src/main/scala/sangria/schema/Schema.scala b/modules/core/src/main/scala/sangria/schema/Schema.scala index 6623f86a..38e234ce 100644 --- a/modules/core/src/main/scala/sangria/schema/Schema.scala +++ b/modules/core/src/main/scala/sangria/schema/Schema.scala @@ -1532,21 +1532,43 @@ case class Schema[Ctx, Val]( } } - lazy val implementations: Map[String, Vector[ObjectType[_, _]]] = - allImplementations - .map { case (k, xs) => - ( - k, - xs.collect { case obj: ObjectType[_, _] => - obj - }) - } - .filter { case (_, v) => - v.nonEmpty - } + def isPossibleImplementation(baseTypeName: String, tpe: ObjectLikeType[_, _]): Boolean = + tpe.name == baseTypeName || allImplementations + .get(baseTypeName) + .exists(_.exists(_.name == tpe.name)) + + @deprecated("Use concreteImplementations instead", "4.0.0") + lazy val implementations: Map[String, Vector[ObjectType[_, _]]] = concreteImplementations + + lazy val concreteImplementations: Map[String, Vector[ObjectType[_, _]]] = allImplementations + .map { case (k, xs) => + ( + k, + xs.collect { case obj: ObjectType[_, _] => + obj + }) + } + .filter { case (_, v) => + v.nonEmpty + } + /** This contains the map of all the concrete types by supertype. + * + * The supertype can be either a union or interface. + * + * According to the spec, even if an interface can implement another interface, this must only + * contains concrete types + * + * @see + * https://spec.graphql.org/June2018/#sec-Union + * @see + * https://spec.graphql.org/June2018/#sec-Interface + * + * @return + * Map of subtype by supertype name + */ lazy val possibleTypes: Map[String, Vector[ObjectType[_, _]]] = - implementations ++ unionTypes.values.map(ut => ut.name -> ut.types.toVector) + concreteImplementations ++ unionTypes.values.map(ut => ut.name -> ut.types.toVector) def isPossibleType(baseTypeName: String, tpe: ObjectType[_, _]): Boolean = possibleTypes.get(baseTypeName).exists(_.exists(_.name == tpe.name)) diff --git a/modules/core/src/main/scala/sangria/validation/TypeComparators.scala b/modules/core/src/main/scala/sangria/validation/TypeComparators.scala index c8d460af..46d4b59a 100644 --- a/modules/core/src/main/scala/sangria/validation/TypeComparators.scala +++ b/modules/core/src/main/scala/sangria/validation/TypeComparators.scala @@ -22,7 +22,10 @@ object TypeComparators { case (sub, OptionInputType(ofType2)) => isSubType(schema, sub, ofType2) case (ListType(ofType1), ListType(ofType2)) => isSubType(schema, ofType1, ofType2) case (ListInputType(ofType1), ListInputType(ofType2)) => isSubType(schema, ofType1, ofType2) - case (t1: ObjectType[_, _], t2: AbstractType) => schema.isPossibleType(t2.name, t1) + case (t1: ObjectType[_, _], t2: AbstractType) => + schema.isPossibleType(t2.name, t1) + case (t1: InterfaceType[_, _], t2: AbstractType) => + schema.isPossibleImplementation(t2.name, t1) case (t1: Named, t2: Named) => t1.name == t2.name case _ => false } diff --git a/modules/derivation/src/test/scala/sangria/schema/SchemaConstraintsSpec.scala b/modules/derivation/src/test/scala/sangria/schema/SchemaConstraintsSpec.scala index cb79bb03..aa1e155e 100644 --- a/modules/derivation/src/test/scala/sangria/schema/SchemaConstraintsSpec.scala +++ b/modules/derivation/src/test/scala/sangria/schema/SchemaConstraintsSpec.scala @@ -971,6 +971,84 @@ class SchemaConstraintsSpec extends AnyWordSpec with Matchers { } """) + "rejects an Interface with a differently typed Interface field" in invalidSchema( + graphql""" + type Query { + test: AnotherObject + } + + type A { foo: String } + type B { foo: String } + + interface AnotherInterface { + field: A + } + + type AnotherObject implements AnotherInterface { + field: B + } + """, + "AnotherInterface.field expects type 'A', but AnotherObject.field provides type 'B'." -> Seq( + Pos(14, 11), + Pos(10, 11)) + ) + + "accepts an Interface with a subtyped Interface field (interface)" in validSchema(graphql""" + type Query { + test: Foo + } + + interface Node { + id: ID! + } + + interface Edge { + cursor: String! + node: Node! + } + + interface Foo implements Node { + id: ID! + } + + type ConcreteFoo implements Foo & Node { + id: ID! + } + + type FooEdge implements Edge { + cursor: String! + node: Foo! + } + + type Test { + fooEdge: FooEdge + } + """) + + "accepts an Interface with a subtyped Interface field (union)" in validSchema(graphql""" + type Query { + fooBar: FooBar + } + + type SomeObject { + field: String + } + + union SomeUnionType = SomeObject + + interface Foo { + foo: SomeUnionType + } + + interface Bar implements Foo { + foo: SomeUnionType + } + + type FooBar implements Foo & Bar { + test: String + } + """) + "rejects an Interface missing an Interface argument" in invalidSchema( graphql""" type Query { @@ -995,6 +1073,29 @@ class SchemaConstraintsSpec extends AnyWordSpec with Matchers { ) ) + "rejects an Interface with an incorrectly typed Interface argument" in invalidSchema( + graphql""" + type Query { + fooBar: FooBar + } + + interface Foo { + foo(input: String): String + } + + interface Bar implements Foo { + foo(input: Int): String + } + + type FooBar implements Foo & Bar { + test: Int + } + """, + "Foo.foo(input) expects type 'String', but Bar.foo(input) provides type 'Int'." -> Seq( + Pos(7, 15), + Pos(11, 15)) + ) + "rejects an Interface with an incorrectly typed Interface field" in invalidSchema( graphql""" type Query {