Sangria で以下のようなカスタム Scalar 型を定義してみました。
- 日付用の型で GraphQL 上は文字列として表現し、内部的に
java.time.OffsetDateTime
を使う
はじめに
前回と同様に、以下の環境で実行しました。
build.sbt
ThisBuild / scalaVersion := "3.2.1"
libraryDependencies ++= Seq(
"org.sangria-graphql" %% "sangria" % "3.4.1",
"org.sangria-graphql" %% "sangria-circe" % "1.3.2"
)
結果を JSON 文字列化するため sangria-circe
を使っています。
日付型の実装
カスタム Scalar 型は ScalarType
を使って以下の変換処理を実装する事で定義できます。
- coerceOutput(内部データを GraphQL 上の表現へ変換)
(T, Set[MarshallerCapability]) => Any
- coerceUserInput(変数利用時の GraphQL 上の値を内部データへ変換)
Any => Either[Violation, T]
- coerceInput(GraphQL 上の値を内部データへ変換)
ast.Value => Either[Violation, T]
coerceUserInput
と coerceInput
の違いは引数の型となっており、前者は変数利用の際に、後者は GraphQL へ組み込まれた値を処理する際にそれぞれ使用されるようです。
Date 型の実装例
// エラー定義
case object DateCoercionViolation extends ValueCoercionViolation("invalid date")
// OffsetDateTime への変換処理
val parseDate = (d: String) =>
try
Right(OffsetDateTime.parse(d, DateTimeFormatter.ISO_DATE_TIME))
catch
case _ => Left(DateCoercionViolation)
// Date 型の定義
val DateType = ScalarType[OffsetDateTime](
"Date",
// 内部データを GraphQL 上の表現へ変換
coerceOutput = (d, _) => d.format(DateTimeFormatter.ISO_DATE_TIME),
// GraphQL 上の値を内部データへ変換(変数利用時)
coerceUserInput = {
case s: String => parseDate(s)
case _ => Left(DateCoercionViolation)
},
// GraphQL 上の値を内部データへ変換
coerceInput = {
case sangria.ast.StringValue(s, _, _, _, _) => parseDate(s)
case _ => Left(DateCoercionViolation)
}
)
次の Query を使って Date 型を動作確認してみます。
Query 定義
val QueryType = ObjectType("Query", fields[Unit, Unit](
Field("now", DateType, resolve = _ => OffsetDateTime.now()),
Field(
"addDays",
DateType,
arguments = List(Argument("date", DateType), Argument("n", IntType)),
resolve = ctx => ctx.args.arg[OffsetDateTime]("date").plusDays(ctx.args.arg[Int]("n"))
)
))
ちなみに、GraphQL で表現すると下記のようになると思います。
scalar Date
type Query {
now: Date!
addDays(date: Date!, n: Int!): Date!
}
動作確認1
now の結果はこのようになります。
処理1
val schema = Schema(QueryType)
given ExecutionContext = ExecutionContext.global
val r1 = Executor.execute(schema, graphql"{ now }")
r1.foreach(println(_))
結果1
{
"data" : {
"now" : "2023-01-07T21:19:03.003669+09:00"
}
}
動作確認2
addDays
の結果です。
日付の値を GraphQL クエリへ組み込んでいるので coerceInput
で処理されます。
処理2
val r2 = Executor.execute(schema, graphql"""{ addDays(date: "2023-02-10T15:10:00+09:00", n: 2) }""")
r2.foreach(println(_))
結果2
{
"data" : {
"addDays" : "2023-02-12T15:10:00+09:00"
}
}
動作確認3
日付のフォーマットを変えてみます。
処理3
val r3 = Executor.execute(schema, graphql"""{ addDays(date: "2023-03-10T17:10:00Z", n: 3) }""")
r3.foreach(println(_))
結果3
{
"data" : {
"addDays" : "2023-03-13T17:10:00Z"
}
}
動作確認4
日付のフォーマットに問題がある(ISO_DATE_TIME でパースできない)場合の結果です。
処理4
val r4 = Executor.execute(schema, graphql"""{ addDays(date: "2023-04-10 06:10:00", n: 4) }""")
r4.recover {
case e: ErrorWithResolver => e.resolveError
}.foreach(println(_))
結果4
{
"data" : null,
"errors" : [
{
"message" : "Expected type 'Date!', found '\"2023-04-10 06:10:00\"'. invalid date (line 1, column 17):\n{ addDays(date: \"2023-04-10 06:10:00\", n: 4) }\n ^",
"locations" : [
{
"line" : 1,
"column" : 17
}
]
}
]
}
動作確認5
変数利用時の結果です。
この場合は coerceUserInput
で処理されます。
処理5
val q = graphql"""
query ($$d: Date!) {
addDays(date: $$d, n: 5)
}
"""
val vs = InputUnmarshaller.mapVars("d" -> "2023-05-10T17:30:00+09:00")
val r5 = Executor.execute(schema, q, variables = vs)
r5.foreach(println(_))
結果5
{
"data" : {
"addDays" : "2023-05-15T17:30:00+09:00"
}
}
今回のサンプルコードは https://github.com/fits/try_samples/tree/master/blog/20230107/sangria_custom_scalar