SangriaでGraphQLのinterfaceを扱うには
2018-07-01
QiitaScalaGraphQLsangriasangria-graphql/sangriaを使って GraphQL な API を実装する時に interface をどうやって使うか、という話。
interface 自体は特に難しい話ではないが、地味に動かなくて困ったので残しておく。
まとめ
- interface の実装自体は
InterfaceType
を使うだけ Field.fieldType
にInterfaceType
を与えるだけだと Scheme がエラーになる- 解決策として
Schema.additionalTypes
あるいはInterfaceType.withPossibleTypes
でその interface を実装したObjectType
を与えるUnionType
を使う
interface の実装としての InterfaceType
GraphQL の公式ページ: Schemas and Types | GraphQL いわゆる interface 的な型/機能が GraphQL にも定義されている。
Sangria で InterfaceType を使ったスキーマを実装する
まずは Scala で interface 的な trait を使ったコードを用意する。
trait Animal {
def id: String
def name: String
}
case class Dog(id: String, name: String, kind: String) extends Animal
case class Cat(id: String, name: String, color: Color) extends Animal
sealed abstract class Color(val rgb: String)
object Color {
case object White extends Color("#FFFFFF")
case object Black extends Color("#000000")
case object Brown extends Color("#A52A2A")
}
Animal
な interface をDog
とCat
が implements していてそれぞれ独自のフィールドも持っている。
これらに対する GraphQL スキーマを実装すると以下のようになる。
import sangria.macros.derive
import sangria.schema._
lazy val animalInterface: InterfaceType[Unit, Animal] = InterfaceType[Unit, Animal](
"Animal",
"animal interface",
fields[Unit, Animal](
Field("id", StringType, resolve = ctx => ctx.value.id),
Field("name", StringType, resolve = ctx => ctx.value.name)
)
)
lazy val dogType = derive.deriveObjectType[Unit, Dog](
derive.Interfaces[Unit, Dog](animalInterface)
)
implicit lazy val colorEnum = derive.deriveEnumType[Color]()
lazy val catType = derive.deriveObjectType[Unit, Cat](
derive.Interfaces[Unit, Cat](animalInterface)
)
Scheme をschema.renderPretty
すると以下のようになる。
interface Animal {
id: String!
name: String!
}
type Cat implements Animal {
id: String!
name: String!
color: Color!
}
enum Color {
White
Black
Brown
}
type Dog implements Animal {
id: String!
name: String!
kind: String!
}
interface を返却するディレクティブを定義する
先程定義したanimalInterface
を返却するField
を持つ Query を実装する。
Ctx
はUnit
で、all
に対して固定でDog
とCat
の List を返し、そのField
の型はListType(animalInterface)
にしてある。
lazy val animalQuery: ObjectType[Unit, Unit] = {
ObjectType.apply(
"AnimalQuery",
fields[Unit, Unit](
Field("all", ListType(animalInterface), resolve = { _ =>
Dog("dog-1", "alice", "golden") ::
Dog("dog-2", "bob", "Chihuahua") ::
Cat("cat-1", "charlie", Color.Brown) :: Nil
})
)
)
}
val schema = Schema(animalQuery)
Dog
もCat
もdotType
, catType
でanimalInterface
を implements しているので動きそうに見える。
しかし動かして見るとエラーになってしまう。
sangria.schema.SchemaValidationException: Schema does not pass validation. Violations: Interface 'Animal' must be implemented by at least one object type.
エラーメッセージの通りで、animalInterface: InterfaceType[Unit, Animal]
を implements したObjectType
が無いということ。
これの解決策はいくつかある
- Schema.additionalTypes を使う
- InterfaceType.withPossibleTypes を使う
- UnionType を使う
Schema.additionalTypes を使う
dogType
とcatType
は確実にInterfaceType[Unit, Animal]
を implements しているが、それがSchema
に伝わっていないのが原因。
それを解決するためにSchema.additionalTypes
というフィールドがある。
ソースコードはこのあたり:
sangria/Schema.scala
つまり、以下のように書くだけで良い。
Schema(animalQuery, additionalTypes = dogType :: catType :: Nil)
これで以下のようなクエリを記述することが出来るようになる。
query {
all {
id
name
... on Dog {
kind
}
... on Cat {
color
}
}
}
InterfaceType.withPossibleTypes を使う
Schema に Interface を実装しているObjectType
を伝えるもう一つの方法。
InterfaceType.withPossibleTypes
を利用するとInterfaceType
のインスタンス自体にそれを実装している型をもたせることが可能。
animalInterface
の実装全体は以下のようになる。
lazy val animalInterface: InterfaceType[Unit, Animal] = InterfaceType[Unit, Animal](
"Animal",
"animal interface",
fields[Unit, Animal](
Field("id", StringType, resolve = ctx => ctx.value.id),
Field("name", StringType, resolve = ctx => ctx.value.name)
)
).withPossibleTypes(() => List(dogType, catType))
// SchemaにadditionalTypeを与える必要はない。
Schema(animalQuery)
なお、withPossibleTypes
には 2 つ API があって可変長引数を与える方の API だと StackOverflowError が発生してしまった。
// StackOverflowError!
.withPossibleTypes(dogType, catType)
UnionType を使う
クエリの返り値として interface を返すのを諦めて UnionType を用いる方法。
今回だとdogType
とcatType
のUnionType
を実装すれば良い。
val animalUnionType = UnionType[Unit](
"AnimalUnion",
types = dogType :: catType :: Nil
)
一応クエリの方も載せておくと、Field.fieldType
に UnionType を使うだけ。
それ以外は変えていない。
lazy val animalQuery: ObjectType[Unit, Unit] = {
ObjectType.apply(
"AnimalQuery",
fields[Unit, Unit](
Field("all", ListType(animalUnionType), resolve = { _ =>
Dog("dog-1", "alice", "golden") ::
Dog("dog-2", "bob", "Chihuahua") ::
Cat("cat-1", "charlie", Color.Brown) :: Nil
})
)
)
}
val schema = Schema(animalQuery)
しかし、こうするとクエリの幅が狭くなってしまうし、interface を使っている利点はなくなってしまう。
query {
all {
id # ←ここがエラーになる
... on Dog {
kind
}
... on Cat {
color
}
}
}
番外編
バリデーションエラーになるならバリデーションをやめてしまえばいい、というのもある。
つまり、interface に対して実装が見つからないのを許容するということ。
Schema
にはvalidationRules
というフィールドがあるので、そこで今回邪魔になるルールを除去してみる。
Schema(animalQuery,
validationRules = List(
DefaultValuesValidationRule,
InterfaceImplementationValidationRule,
// InterfaceMustHaveImplementationValidationRule,
SubscriptionFieldsValidationRule,
SchemaValidationRule.defaultFullSchemaTraversalValidationRule
))
これでスキーマのバリデーションはパスするが、実際に動かしてみるとエラーになる。
sangria.execution.UndefinedConcreteTypeError: Can't find appropriate subtype of an interface type 'Animal' for value of class 'net.petitviolet.prac.graphql.sample.UnionInterfaceSampleSchema$Cat' at path 'all[2]'. Possible types: none. Got value: Cat(cat-1,charlie,Brown).
animalInterface
だけだとdogType
とcatType
の情報が入っていないので、レスポンスを返却する時に sangria が判断できずにエラーになってしまう。
from: https://qiita.com/petitviolet/items/6b8ac832360408a323a1