graphql-ruby と graphql-codegen で Date/DateTime 型のフィールドを安全に扱う

## 背景
- [graphql-ruby](https://graphql-ruby.org/) と [graphql-codegen](https://www.the-guild.dev/graphql/codegen) を使ってスキーマの型を TypeScript 向けに自動生成している
- その中で Date/DateTime 型のフィールドをうまく扱うにはどうしたらいいか調べた

## 環境
- graphql-ruby 2.0.11
- @graphql-codegen/cli 2.8.0
- @graphql-codegen/typescript 2.7.1
- @graphql-codegen/typescript-urql 3.6.1

## 課題
graphql-ruby で Date/DateTime 型のフィールドがある場合、標準では Date のような Scalar 型が無いため、愚直に書くと下記のようになる。

```rb
module Types
  class UserType < Types::BaseObject
    field :name, String, null: false
    field :created_at, String, null: false
    field :updated_at, String, null: false

    def created_at
      object.created_at.iso8601
    end

    def updated_at
      object.updated_at.iso8601
    end
  end
end
```

いちいち ISO8601 形式に変換するためにフィールドごとにメソッドを定義しなければならないので、何とかしたい。

## GraphQL::Types::ISO8601DateTime クラスを利用する
少し調べてみると [GraphQL::Types::ISO8601Date](https://graphql-ruby.org/api-doc/2.0.11/GraphQL/Types/ISO8601Date.html) / [GraphQL::Types::ISO8601DateTime](https://graphql-ruby.org/api-doc/2.0.11/GraphQL/Types/ISO8601DateTime.html) というクラスが存在することが分かった。

これは下記のように型の指定で使うことができ、 field の中身が DateTime 型だった時に ISO8601 形式の文字列に変換してくれる。

```rb
module Types
  class UserType < Types::BaseObject
    field :name, String, null: false
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
  end
end
```

query:

```graphql
query getUser {
  user { name createdAt updatedAt }
}
```

返ってくる JSON:

```json
{
  "data": {
    "user": {
      "name": "User Name",
      "createdAt": "2022-10-08T20:25:10Z",
      "updatedAt": "2022-10-08T20:25:10Z"
    }
  }
}
```

## graphql-codegen で string 型として扱う
ここまでは良いのだが、このまま graphql-codegen で TypeScript の型を生成しようとすると、 createdAt と updatedAt が any 型になってしまう。

```ts:src/generated/graphql.ts
export type User = {
  __typename?: 'User';
  name: string;
  createdAt: any;
  updatedAt: any;
};
```

実際に返ってくる型は string 型なので、間違って Date 型などと勘違いしたコードを書かないように、しっかり string 型であることを型定義に入れておきたい。
そこで設定ファイル codegen.yml に下記のような設定を追加する。

```yaml:codegen.yml
generates:
  src/generated/graphql.ts:
    config:
      scalars:
        ISO8601Date: string
        ISO8601DateTime: string
```

これはカスタム Scalar 型の ISO8601Date 型を string と見なすこと、という指示を graphql-codegen に対して与えている。
これを追加した上で graphql-codegen を実行すると、下記のように string 型として定義される。

```ts:src/generated/graphql.ts
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
  ISO8601DateTime: string; // Scalars['ISO8601DateTime'] が string に変換される
};

export type User = {
  __typename?: 'User';
  name: string;
  createdAt: Scalars['ISO8601DateTime']; // any ではなく Scalars['ISO8601DateTime'] として定義される
  updatedAt: Scalars['ISO8601DateTime'];
};
```

こうして Date/DateTime 型のフィールドを手間なく string に変換して JSON に含めることができ、かつフロントエンドでも string 型として安全に扱うことができる。

なお今回は field として Date/Datetime 型のデータを扱う方法について書いたが、 GraphQL::Types::ISO8601DateTime は argument として受け取った文字列を DateTime 型に変換して resolver に渡すといった機能もあるはずである。
今回は使わなかったので特に検証などもしなかったが、もし Date/DateTime 型のデータをフロントから送信する必要がある場合は利用してみようと思う。

## appendix: timestamp field を定義する処理を共通化する
Rails を扱っている限り created_at/updated_at という field を持つデータを数多く扱うことになると思うので、上記のような定義をいちいち毎回書くのが面倒だなと思い、次のような module を定義した。

```rb:app/graphql/types/concerns/timestamp_fields.rb
module Types
  module Concerns
    module TimestampFields
      extend ActiveSupport::Concern

      included do
        field :created_at, GraphQL::Types::ISO8601DateTime, null: false
        field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
      end
    end
  end
end
```

これを下記のように各リソース用のクラスで include する。

```rb:app/graphql/types/user_type.rb
module Types
  class UserType < Types::BaseObject
    include Types::Concerns::TimestampFields

    field :name, String, null: false
  end
end
```

これでモデルごとに毎回 created_at / updated_at を定義する必要がなく、 TimestampFields モジュールを include するだけで同じフィールドを定義することができる。便利。