背景
- graphql-ruby と 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 型が無いため、愚直に書くと下記のようになる。
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 / GraphQL::Types::ISO8601DateTime というクラスが存在することが分かった。
これは下記のように型の指定で使うことができ、 field の中身が DateTime 型だった時に ISO8601 形式の文字列に変換してくれる。
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:
query getUser {
user { name createdAt updatedAt }
}
返ってくる 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 型になってしまう。
export type User = {
__typename?: 'User';
name: string;
createdAt: any;
updatedAt: any;
};
実際に返ってくる型は string 型なので、間違って Date 型などと勘違いしたコードを書かないように、しっかり string 型であることを型定義に入れておきたい。
そこで設定ファイル codegen.yml に下記のような設定を追加する。
generates:
src/generated/graphql.ts:
config:
scalars:
ISO8601Date: string
ISO8601DateTime: string
これはカスタム Scalar 型の ISO8601Date 型を string と見なすこと、という指示を graphql-codegen に対して与えている。
これを追加した上で graphql-codegen を実行すると、下記のように string 型として定義される。
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 を定義した。
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 する。
module Types
class UserType < Types::BaseObject
include Types::Concerns::TimestampFields
field :name, String, null: false
end
end
これでモデルごとに毎回 created_at / updated_at を定義する必要がなく、 TimestampFields モジュールを include するだけで同じフィールドを定義することができる。便利。