GraphQLのmutationでargumentにオブジェクトを渡す

graphql-ruby で Mutation を書いていて、 argument にスカラ型ではなくオブジェクトを渡す時に variables を併用する場合の書き方で詰まったのでメモを残しておく。

## 環境
- Rails 6.0.0
- graphql-ruby 1.9.14

## 目標
- createUser という mutation の argument にオブジェクトを渡す
- クライアント側のクエリでは variables を用いる

## 手順
### MutationType にフィールドを追加する
```rb:app/graphql/types/mutation_type.rb
module Types
  class MutationType < Types::BaseObject
    field :createUser, mutation: Mutations::CreateUserMutation
  end
end
```

- mutation_type.rb に `createUser` というフィールドを追加し、 Mutations::CreateUserMutation を紐付ける

### CreateUserMutation を実装する
```rb:app/graphql/mutations/create_user_mutation.rb
module Mutations
  class CreateUserMutation < GraphQL::Schema::RelayClassicMutation
    graphql_name "CreateUser"

    field :user, Types::UserType, null: true
    field :error, String, null: true

    argument :user, Types::Attributes::UserInput, required: true

    def resolve(user:)
      created_user = User.create(user.to_h.indifferent_access.transform_keys(&:underscore))

      { user: created_user }
    rescue ActiveRecord::RecordInvalid => error
      { error: error.message }
    end
  end
end
```
- `field :user` は、この mutation 実行後のレスポンスとして取得できるフィールド。
- `Types::UserType` は普段 query で使用する BaseObject なので内容は割愛。
- `argument :user` が本題。 user というリクエストパラメータを受け取ることを宣言。そのオブジェクトの方を `Types::Attributes::UserInput` というクラスで定義している（詳細は後述）。
- 上記で宣言したリクエストパラメータを `resolve` メソッドのキーワード引数で受け取る。

ちなみに user の中身はハッシュではなく `Types::Attributes::UserInput` のインスタンスになっている。
そのため中のプロパティにアクセスするには下記の二通りある。

```rb
user.postal_code # インスタンスメソッドを通してアクセス
user[:postalCode] # ハッシュのキーを通してアクセス（この時、プロパティ名はキャメルケースにする）
```

単純に `to_h` するだけだとキーがキャメルケースのままなので、 `with_indifferent_access.transform_keys(&:underscore)` という長ったらしい変換処理が必要になる。
当然、他の mutation 内でも使うことが予想されるので、 Types::BaseInputObject クラス内にインスタンスメソッドを定義してしまうと良いと思う。

```rb:app/graphql/types/base_input_object.rb
module Types
  class BaseInputObject < GraphQL::Schema::InputObject
    argument_class Types::BaseArgument

    def to_params
      to_h.with_indifferent_access.transform_keys(&:underscore)
    end
  end
end
```

これなら `user.to_h.indifferent_access.transform_keys(&:underscore)` を `user.to_params` に置き換えられる。

### Types::Attributes::UserInput を実装する
```rb:app/graphql/types/attributes/user_input.rb
class Types::Attributes::UserInput < Types::BaseInputObject
  argument :name, String, required: true
  argument :gender, Integer, required: true
  argument :profile, String, required: true
  argument :postal_code, String, required: true
end
```

- 先述した resolve メソッドで受け取るキーワード引数の型を下記のように定義する。型の書き方などは query_types 等と同様。
- さらにオブジェクトをネストしたい時は Types::BaseInputObject を継承する別のクラスを指定するのかな？（試してない）

以上でサーバー側の実装は完了。

### クライアント側から送信する query を書く
```graphql
mutation registerUser(
  $user: UserInput!
) {
  createUser(input: { user: $user }) {
    user { id postalCode profile }
  }
}
```

- 1行目の `registerUser` はこのクエリに付けた適当な名前なので変えても動く。
- 2行目の `UserInput!` の `!` がキモ。 `Mutations::CreateUserMutation` の `argument` で `required: true` を指定した場合、この `!` を付けないと `Nullability mismatch on variable $user` というエラーが出る（ここで1時間くらいハマった）。
- 4行目の `createUser` が Types::MutationType で定義した mutation 名。
- 5行目の `user { id postalCode profile }` が mutation 実行後のレスポンスボディに入れてほしいオブジェクトとフィールドの指定。

### クエリを実行
```rb
query = <<-QUERY
  mutation registerUser(
    $user: UserInput!
  ) {
    createUser(input: { user: $user }) {
      user { id postalCode profile }
    }
  }
QUERY
variables = {
  karte: {
    name: "midwhite",
    gender: 1,
    profile: "I am a software engineer."
    postalCode: "000-0000"
  }
}
AppSchema.execute(query, variables: variables).to_h
```

- variables に user オブジェクトとして渡したいパラメータをハッシュで記述する。

実際にはフロントから Apollo とか使って query や variables を送信するんだと思うけど、 query の形式さえ分かっていれば特に迷わないだろうと思うのでその部分は割愛。
ここまで分かれば mutation を実用レベルで書けそう。そろそろエンドポイントを GraphQL のみで実装したアプリケーションを書いてみたい気持ち。