[Scala]Sangriaを使ってGraphQL APIを実装する

## この記事はなに？

ScalaでGraphQLサーバを実装するための[sangria/sangria](https://github.com/sangria-graphql/sangria)の導入。  

[公式ドキュメント](http://sangria-graphql.org)および[Larning Sangria](http://sangria-graphql.org/learn/)を読むのが一番速い。
さらに公式がサンプルも用意しているので、そちらを参照するのが良い。  
[sangria-graphql/sangria-akka-http-example](https://github.com/sangria-graphql/sangria-akka-http-example/tree/master/src/main/scala)

が、簡単に一覧出来るように短めのサンプルを載せておく。

## 使い方のサンプル

### Query/Mutationを投げられるようになるまで

ある独自のオブジェクトに対してQueryおよびMutationを投げられるようにするまで。
ソースコードは[Github](https://github.com/petitviolet/scala-graphql-prac/blob/e7452ceb8555354f72d00a7e1f56538be88d807b/modules/main/src/main/scala/net/petitviolet/prac/graphql/sample/sample.scala)に置いた。  

まずはGraphQLでやり取りしたいクラスを定義。

```scala
  case class MyObject(id: Long, name: String)
  class MyObjectRepository() {  // DBへのアクセスをするやつ
    def findAll: Seq[MyObject] = ???
    def findById(id: Long): Option[MyObject] = ???
    def store(obj: MyObject): MyObject = ???
    def create(name: String): MyObject = ???
  }
```

この`MyObject`に対応するGraphQLスキーマを定義していく。  
case classを単純に変換するだけなら`sangria.macros.derive.deriveObjectType`を使うと機械的に実装できる。

```scala
  // MyObjectをGraphQL的に表現する
  // macroを使って楽に導出する
  val myObj = sangria.macros.derive.deriveObjectType[Unit, MyObject]()
```

続いて、`all`で`MyObjectRepository.findAll`、`find_by_id(id)`で`MyObjectRepository.findById(id)`の結果を返却するようなQueryを実装する。

```scala
  // MyObjectに対するQuery
  lazy val myQuery: ObjectType[MyObjectRepository, Unit] = {
    ObjectType.apply(
      "MyQuery",
      fields[MyObjectRepository, Unit](
        Field("all", ListType(myObj), resolve = c => c.ctx.findAll), // allで全部返す
        {
          val idArg = Argument("id", LongType)
          Field("find_by_id", OptionType(myObj), arguments = idArg :: Nil, 
            resolve = c => c.ctx.findById(ctx arg idArg)) // find_by_id(id)でid指定して取得する
        }
      )
    )
  }
```

Queryを実行するためには`MyObjectRepository`が必要なので、`ObjectType`の`Ctx`型パラメータは`MyObjectRepository`を与える。  
`Field`の第二引数で返却するデータの型を表し、`arguments`引数ではこのQueryを実行するための入力、`resolve`には実際に`MyObjectRepository`を使ってデータにアクセスする関数を与える。

Queryが実装できたので次はMutation。
`MyObject`の情報を貰って保存する`store`とnameだけもらって残りは生成する`create`を実装してみる。

`store`のために`MyObject`を入力として受け取れるようにする必要があるため、そのあたりから。

```scala
  // MyObjectをinputとして受け取れるようにする
  // ここはもう少しいいやり方があるかも知れない...
  val myObjectInputType: InputObjectType[MyObject] =
  InputObjectType[MyObject]("MyObjectInput",
                            List(
                              InputField("id", LongType),
                              InputField("name", StringType)
                            ))

implicit val myObjectInput: FromInput[MyObject] = new FromInput[MyObject] {
  override val marshaller: ResultMarshaller = CoercedScalaResultMarshaller.default

  override def fromResult(node: marshaller.Node): MyObject = {
    val m = node.asInstanceOf[Map[String, Any]]
    MyObject(m("id").asInstanceOf[Long], m("name").asInstanceOf[String])
  }
}
```

これで`MyObject`を受け取れるようになったので、Mutationを実装する。  
Queryと同様に`MyObjectRepository`を`ObjectType`の`Ctx`型パラメータとして渡しておき、`Field.resolve`で使えるようにしておく。

```scala
// mutation
lazy val myMutation: ObjectType[MyObjectRepository, Unit] = {
  ObjectType.apply(
    "MyMutation",
    fields[MyObjectRepository, Unit](
      {
        // my_objectにJSONでMyObjectのデータをもらって追加保存する
        val inputMyObject = Argument("my_object", myObjectInputType)
        Field(
          "store",
          arguments = inputMyObject :: Nil,
          fieldType = myObjectType,
          resolve = c => c.ctx.store(c arg inputMyObject)
        )
      }, {
        // nameだけ貰って新規作成する
        val inputName = Argument("name", StringType)
        Field(
          "create",
          arguments = inputName :: Nil,
          fieldType = myObjectType,
          resolve = c => c.ctx.create(c arg inputName)
        )
      }
    )
  )
}
```

ここまでで実装したQueryとMutationをGraphQLとして受け付けられるようにするには`Schema`を使う。

```scala
  // myQueryとmyMutationをGraphQLのSchemaとする
  lazy val schema: Schema[MyObjectRepository, Unit] = Schema(myQuery, Some(myMutation))
}
```

これで`MyObject`に対してQueryとMutationを実行する準備が出来た。

### Akka-HTTPでGraphQL APIを公開する

Akka-HTTPとspray-jsonを使ってGraphQLを実行するサンプルがこんな感じになる。  
`src/main/resources`配下に[graphiql.html](https://github.com/sangria-graphql/sangria-akka-http-example/blob/master/src/main/resources/graphiql.html)を配置してある。

```scala
// POST: /graphqlで受け付ける
val route: Route = (post & path("graphql")) {
  entity(as[JsValue]) { jsObject =>
    complete(this.execute(jsObject)(executionContext))
  } ~
  get {
    getFromResource("graphiql.html")
  }
}
val repository = new SchemaSample.MyObjectRepository
  
// Query or Mutationを受け取ってSchemaSample.schemaで実行する
def execute(jsValue: spray.json.JsValue)(implicit ec: ExecutionContext): Future[(StatusCode, JsValue)] = {
  val JsObject(fields) = jsValue
  val operation = fields.get("operationName") collect {
    case JsString(op) => op
  }

  val vars = fields.get("variables") match {
    case Some(obj: JsObject) => obj
    case _                   => JsObject.empty
  }

  // queryかmutationのどちらか
  val Some(JsString(document)) = fields.get("query") orElse fields.get("mutation")

  Future.fromTry(QueryParser.parse(document)) flatMap { queryDocument =>
    import StatusCodes._
    // 実装したSchemaとDBアクセスのためのRepositoryを渡して実行する
    Executor
      .execute(
        SchemaSample.schema, queryDocument, repository
        operationName = operation, variables = vars
      )
      .map { jsValue => OK -> jsValue }
      .recover {
        case error: QueryAnalysisError => BadRequest -> error.resolveError // リクエストが不正な場合
        case error: ErrorWithResolver  => InternalServerError -> error.resolveError // データを取得できなかった場合
      }
  }
}
```

これをよしなに起動して実行してQuery/Mutationを投げてみる。

### 実行結果サンプル

Queryを投げてみるには以下のようなリクエストを送る。

```
query MyQuery {
  all {
    ...MyObj
  }
  find_by_id(id: 2) {
    ...MyObj
  }
}

fragment MyObj on MyObject {
  id
  name
}
```

こういうfragmentのようなものが用意されていてGraphQLはよい。
結果はJSONで返ってきて、サンプルとしてはこんな感じになる。

```json
{
  "data": {
    "all": [
      {
        "id": 1,
        "name": "alice"
      },
      {
        "id": 2,
        "name": "bob"
      },
    ],
    "find_by_id": {
      "id": 2,
      "name": "bob"
    }
  }
}
```

続いてMutation。

```
mutation MyMutation {
  store(my_object: {id: 3, name: "charlie"}) {
    id
  } 
  create(name: "dave") {
    id
  }
}
```

結果はこんな感じ。

```json
{
  "data": {
    "store": {
      "id": 3
    },
    "create": {
      "id": 4
    }
  }
}
```

それぞれの操作に対する結果が返却されているのがわかる。

Query/Mutationをとりあえず動かすならこれくらいでおおよそ大丈夫なはず。

## CtxとValについての補足

[ドキュメント](http://sangria-graphql.org/learn/#input-and-context-objects)

`ObjectType`や`Field`についてくる`Ctx`と`Val`という型パラメータ。  
実装は[ObjectType](https://github.com/sangria-graphql/sangria/blob/4f96ea218b5db65c57bb296c18e7ded4f5d1a797/src/main/scala/sangria/schema/Schema.scala#L206)と[Field](https://github.com/sangria-graphql/sangria/blob/4f96ea218b5db65c57bb296c18e7ded4f5d1a797/src/main/scala/sangria/schema/Schema.scala#L329)のあたり

`Val`はGraphQLで型として表現したいもの、たとえば`User`とか`Todo`みたいな。
`Field#resolve`で返却するオブジェクトの型を`Val`として扱う。
QueryやMutationを宣言する際には`Unit`でよい。

`Ctx`はGraphQLのクエリを実行するために必要なcontextで、具体的にはデータベースへのアクセスが可能なserviceやrepositoryのようなオブジェクトが与えられる。
DBへのアクセスなしで型を表現できる場合には`Ctx`は`Unit`でよい。
QueryやMutationを実行するには基本的には必要になるもの。