LoginSignup
0
0

More than 1 year has passed since last update.

Sangriaで日付のカスタムScalar型を定義する

Posted at

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]

coerceUserInputcoerceInput の違いは引数の型となっており、前者は変数利用の際に、後者は 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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0