Rails と React+TypeScript でのGraphQLスキーマ運用プラクティス

Linc'well Advent Calendar3日目の記事です。

当社が展開するクリニックグループである [_CLINIC FOR_ の予約システム](https://reservation.clinicfor.life/) では一部機能にGraphQLを導入しているのですが、スキーマの管理等も踏まえてどのように構築し、運用したかについて紹介したいと思います。

**※ ちなみに構築したのは2019/3~4頃であり、その点は割り引いてご覧ください :pray_tone2: 
今だともっと良さげなプラクティスがあるかもしれません。**

また、予約システム全体としてはモノリシックなRailsアプリケーションになっているのですが、GraphQLを導入した一部機能ではフロントエンドがTypeScript+Reactで構築されており、ruby側で定義されたスキーマ情報はTSの型定義へスムーズに繋げる必要がありました。

## スキーマファイルの生成

GraphQLには様々な実装があるかと思いますが、今回[`rmosolgo/graphql-ruby`](https://github.com/rmosolgo/graphql-ruby) というgemを利用しました。

こちらのgemはスキーマの定義ファイルを直接記述せず、個別のクエリや型の実装から最終的なスキーマを出力する _code-first_ なアプローチを取っています。

なので処理を先に記述し、それを参照することで型定義が決定される、という順番になるのですが、最終的な定義の情報は、Schemaのクラスに生えている以下のメソッドで文字列として全出力され、その戻り値をファイル出力してあげることでスキーマファイルが出来上がります。

[`GraphQL::Schema#to_definition`](https://www.rubydoc.info/github/rmosolgo/graphql-ruby/GraphQL%2FSchema:to_definition)

今回は上記メソッドを以下のようなrakeタスクに組み込み、差分が生じたらファイル出力の上でコミットしていくという運用で行くことにしました。

```ruby
# rake graphql:dump_schema
namespace :graphql do
  task dump_schema: :environment do
    schema_definition = LincwellSchema.to_definition
    schema_path = 'frontend/src/generated/schema.graphql'
    File.write(Rails.root.join(schema_path), schema_definition)
    puts "#{schema_path} updated."
  end
end
```

### CIでのスキーマ定義チェック

定義ファイルが最新のものかチェックするため、rspecにてスキーマファイルの最新チェックも用意します。

```ruby
describe 'Validate lastest-veresion' do
  let(:dump_path) { Rails.root.join('frontend', 'src', 'generated', 'schema.graphql') }
  let(:current_definition) { File.read(dump_path) }

  subject { described_class.to_definition }

  it { is_expected.to eq(current_definition) }
end
```

簡易な実装ですが、CIが回るたびにチェックが入るので、少なくともこれで更新忘れはなくなり、実装とスキーマの乖離を防止することができたのではないかと思います。

## スキーマ情報からTSの型をつくる

続いてはこうして出来上がったスキーマ情報をフロントエンド側へ連携させていきます。

出力されたスキーマ情報をもとにTSの型を作りたいのですが、さすがに手ずから行うのは厳しいので [`graphql-codegen`](https://graphql-code-generator.com/)を用いて自動化することにします。


```yaml
# codegen.yml
overwrite: true
schema: "src/generated/schema.graphql"
documents: "src/libs/queries/*.ts"
generates:
  src/generated/graphql.tsx:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"
  src/generated/graphql.schema.json:
    plugins:
      - "introspection"
```

```json
"scripts": {
  "generate": "graphql-codegen --config codegen.yml"
}
```

上記設定の上、 `npm run generate` コマンドを実行します。

そうするとrailsから出力されたスキーマファイルを元に、 `src/generated/graphql.tsx` にTSの型定義が自動生成されるようになります。ここまで繋がると気持ちイイですね！

これでReactコンポーネントから自由に任意のクエリやオブジェクトをTSの型として利用できるようになりました。Rails側で定義されている `graphql/types` と乖離しないため、非常に楽に、かつ楽しく開発を進めることができます。

```json
{
    "paths": {
      "Components/*": ["src/components/*"],
      "Styles/*": ["src/styles/*"],
      "Libs/*": ["src/libs/*"],
      "Generated/*": ["src/generated/*"]
    }
  }
}
```

tsconfig.jsonで上記のようにaliasを切り、Componentで以下のように呼び出せます。

```tsx
import * as React from "react";
import { useQuery } from "react-apollo-hooks";
import styled from "styled-components";
import { CancelRate } from "Generated/graphql";
```

めでたしめでたし :tada: 

ちなみにフロントエンド側の型生成まわりに関しては以下の記事を大変参考にさせて頂きました。

https://qiita.com/mizchi/items/fb9f598cea94d2c8072d

投稿時期も非常にタイムリーで、もしこのエントリがなかったらと思うと(ry

## まとめ

- code-firstな `graphql-ruby` の利用
- `#to_definition` をrake経由で実行し、型定義の `.graphql` ファイルをフロントへ配置
- `rspec` にて最新dumpの検査
- `graphql-codegen` にてTSの型定義ファイルへコンバート
- フロントアプリケーションにて利用可能に

だいたいこんな感じになりました。
現状もこの当時からあまり大きく変わっていませんが、もしより良いプラクティスがあればフィードバック頂けたら幸いです。