railsでのGraphQLサーバーの構築と周辺ツールのセットアップ手順

# やる事

`rails`アプリケーションで`GraphQL`を開発するための初期構築と周辺ツールのセットアップを行います。

1. `graphql` gem のインストール
1. `graphql-ruby`の初期化コマンド実行
1. `AppSchema`クラスの設定項目の変更
1. `rubocop-graphql`のセットアップ
1. スキーマ情報をファイル出力する`rake task`追加

## 1. `graphql` gem のインストール

`GraphQL`サーバーの構築に`graphql`gemを使います

https://github.com/rmosolgo/graphql-ruby

```diff:Gemfile
+ gem 'graphql'
```

```bash
bundle install
```

## 2. `graphql`の初期化コマンド実行

`graphql`gemの機能で`GraphQL`開発のセットアップを行うコマンド`graphql:install`コマンドが`rails generator`として追加されているので使用します。

https://graphql-ruby.org/schema/generators.html#graphqlinstall

```bash
rails g graphql:install --skip-graphiql --skip_keeps --api=true --relay=false --skip_mutation_root_type=true
```

ここでは以下のオプションを指定しています。

- `graphiql`は使用しないので`--skip-graphiql`を指定
- 空ディレクトリを維持する`.keep`ファイルは不要なので`--skip_keeps`を指定
- `api`モードでの実装にするので`--api`オプションを`true`に指定
- `relay`は使用しないので`--relay`オプションを`false`に指定
- `mutation`は今回使わないので`skip_mutation_root_type`オプションを`true`に指定

### 生成ファイル

`app/graphql`ディレクトリの作成などが行われます

```bash
$ tree app/graphql/

app/graphql/
├── app_schema.rb
└── types
    ├── base_argument.rb
    ├── base_enum.rb
    ├── base_field.rb
    ├── base_input_object.rb
    ├── base_interface.rb
    ├── base_object.rb
    ├── base_scalar.rb
    ├── base_union.rb
    └── query_type.rb
```

`graphql`のエンドポイントの定義とコントローラーの雛形も作成されます。

```diff:config/routes.rb
+ post "/graphql", to: "graphql#execute"
```

```ruby:app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
  # If accessing from outside this domain, nullify the session
  # This allows for outside API access while preventing CSRF attacks,
  # but you'll have to authenticate your user separately
  # protect_from_forgery with: :null_session

  def execute
    variables = prepare_variables(params[:variables])
    query = params[:query]
    operation_name = params[:operationName]
    context = {
      # Query context goes here, for example:
      # current_user: current_user,
    }
    result = AppSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
    render json: result
  rescue StandardError => e
    raise e unless Rails.env.development?
    handle_error_in_development(e)
  end

  private

  # Handle variables in form data, JSON body, or a blank value
  def prepare_variables(variables_param)
    case variables_param
    when String
      if variables_param.present?
        JSON.parse(variables_param) || {}
      else
        {}
      end
    when Hash
      variables_param
    when ActionController::Parameters
      variables_param.to_unsafe_hash # GraphQL-Ruby will validate name and type of incoming variables.
    when nil
      {}
    else
      raise ArgumentError, "Unexpected parameter: #{variables_param}"
    end
  end

  def handle_error_in_development(e)
    logger.error e.message
    logger.error e.backtrace.join("\n")

    render json: { errors: [{ message: e.message, backtrace: e.backtrace }], data: {} }, status: 500
  end
end
```

## 3. `AppSchema`クラスの設定項目の変更

自動生成で作成した`app/graphql/app_schema.rb`に設定の追加、不要箇所の削除を行います。

```diff:app/graphql/app_schema.rb
class AppSchema < GraphQL::Schema
+  disable_introspection_entry_points unless Rails.env.development?
+  
   query(QueryType)
+
+  max_complexity 200
+  max_depth 30
+  default_page_size 50
+  
-  # For batch-loading (see https://graphql-ruby.org/dataloader/overview.html)
-  use GraphQL::Dataloader
-
-  # GraphQL-Ruby calls this when something goes wrong while running a query:
-  def self.type_error(err, context)
-    # if err.is_a?(GraphQL::InvalidNullError)
-    #   # report to your bug tracker here
-    #   return nil
-    # end
-    super
-  end
-
-  # Union and Interface Resolution
-  def self.resolve_type(abstract_type, obj, ctx)
-    # TODO: Implement this method
-    # to return the correct GraphQL object type for `obj`
-    raise(GraphQL::RequiredImplementationMissingError)
-  end
-
-  # Stop validating when it encounters this many errors:
  validate_max_errors(100)
end
```

変更後のファイルの内容は以下の通りです。

```ruby:app/graphql/app_schema.rb
class AppSchema < GraphQL::Schema
  disable_introspection_entry_points unless Rails.env.development?

  query(QueryType)
  
  max_complexity 200
  max_depth 30
  default_page_size 50
  validate_max_errors(100)
end
```

これらの設定を適切に行う事で、高負荷なクエリの抑制や、不要な情報の出力を防ぎ堅牢なサーバーに繋がります。
逆に、設定不足や不適切な値の設定をしてしまうことで脆弱性やサービスの安定性にもつながるのでサービスに応じて適切に設定を行うことが必要です。

### 設定した値の詳細

#### `disable_introspection_entry_points`

`GraphQL`スキーマ情報を`Intorospection`として取得可能にするのは開発環境のみで良いので、開発環境以外の場合いは`disable_introspection_entry_points`で無効化しています

https://graphql-ruby.org/schema/introspection#disabling-introspection

#### `max_complexity`
`GeaphQL`はクエリが柔軟な分、複雑なクエリの構築が行えてしまいます。そのためクエリの複雑具合を検出して一定の閾値を設け抑制する設定を`max_complexity`として追加します。
- 同様に、リソースをネストとして無限にデータ検索を行う事も可能なのでネストに制限を加える`max_depth`を指定します

https://graphql-ruby.org/queries/complexity_and_depth

#### `default_page_size`

リソースの件数が大きくなることを想定しページングのサイズのデフォルトを`default_page_size`で指定しています

https://graphql-ruby.org/pagination/using_connections.html#default-page-size

#### `validate_max_errors`

不正な入力が行われ複数のバリデーションエラーが発生する際にエラー数に制限を設定しバリデーションを中断する設定を`validate_max_errors`として設定します

https://graphql-ruby.org/errors/overview.html#validation-errors

## 4. `rubocop-graphql`のセットアップ

`Gemfile`に定義を追加しインストールします。

```diff:Gemfile
+ gem "rubocop-graphql"
```

```bash
bundle install
```

https://github.com/DmitryTsepelev/rubocop-graphql


`rubocop`の設定ファイルで`rubocop-graphql`を読み込むように追記します。

```diff:.rubocop.yml
require:
+  rubocop-graphql
```

`rubocop`を実行し警告が出る場合は修正していきます

```bash
rubocop
```

### 特定のクラスで`diasble`を指定する

以下の二つのクラスで`description`定義の強制が求められますが以下の`class`では定義メソッドが存在しないので`diasble`します。

- `GraphQL::Schema::Field`
- `GraphQL::Schema::Argument`

```diff:app/graphql/types/base_argument.rb
- class BaseArgument < GraphQL::Schema::Argument
+ class BaseArgument < GraphQL::Schema::Argument # rubocop:disable GraphQL/ObjectDescription NOTE: `description`は`GraphQL::Schema::Argument`にはない
end
```


```diff:app/graphql/types/base_field.rb
- class BaseField < GraphQL::Schema::Field
+ class BaseField < GraphQL::Schema::Field # rubocop:disable GraphQL/ObjectDescription NOTE: `description`は`GraphQL::Schema::Field`にはない
end
```

## 5. スキーマ情報をファイル出力する`rake task`追加

フロントエンド開発などで活用可能するために`GraphQL`スキーマ情報をファイル出力する`rake task`追加します。
`graphql-ruby`gemには`rails task`を追加する`GraphQL::RakeTask`クラスが用意されているので、こちらを使用します。


https://graphql-ruby.org/testing/schema_structure.html#maintain-a-graphql-schema-dump

ソースコードはこちら

https://github.com/rmosolgo/graphql-ruby/blob/511ec429a487d8b3d2aed6cce29be596be64ae74/lib/graphql/rake_task.rb



### rake taskの定義

`lib/tasks`配下に`graphql_schema_dump.rake`を作成して以下の様に定義します。

```ruby:lib/tasks/graphql_schema_dump.rake
require 'graphql/rake_task'

load_context = Proc do
  current_user = User.first

  {
    current_user:,
  }
end

GraphQL::RakeTask.new(schema_name: 'AppSchema', load_context:, directory: 'graphql_schema')
```

- `GraphQL::RakeTask`クラスを`new`する事で`rails task`が追加されます。
- `schema_name`はアプリケーションで用いているスキーマクラスの名前`AppSchema`(`app/graphql/app_schema.rb`)を指定します。
- ログイン情報等のコンテキストをスキーマの中で扱っている場合は`load_context`を使ってダミーデータをコンテキストに引き渡す`Proc`を指定する必要があります。
  - ここではDBに存在する`User`オブジェクトを取得して引き渡しています。
- ファイルを出力するディレクトリを`graphql_schema`に指定します

### `rake task`の確認

上記を追加すると以下の様にスキーマ出力に`rake task`が追加されています。

```bash
rails task -T | grep graphql

rails graphql:pro:validate[gem_version]                                            # Get the checksum of a graphql-pro version and compare it to published versions on GitHub and graphql-ruby.org
rails graphql:schema:dump                                                          # Dump the schema to JSON and IDL
rails graphql:schema:idl                                                           # Dump the schema to IDL in graphql_schema/schema.graphql
rails graphql:schema:json                                                          # Dump the schema to JSON in graphql_schema/schema.json
```

### 追加された`rake task`の詳細

#### rails graphql:pro:validate[gem_version]

- Proバージョンを使用している場合にライセンスが有効化の検証をする

https://graphql.pro/

#### rails graphql:schema:dump

- スキーマを`IDL`と`JSON`形式両方でそれぞれ出力する

#### rails graphql:schema:idl

- スキーマを`IDL`形式で出力する

#### rails graphql:schema:json

- スキーマを`JSON`形式で出力する

# 参考

- https://github.com/rmosolgo/graphql-ruby
- https://github.com/DmitryTsepelev/rubocop-graphql
- https://graphql-ruby.org/
- https://graphql.org/

# 開発環境

- graphql (2.1.0)
- rails 7.0.8
- ruby 3.2.2p53