GraphQLでカスタムスカラー型を作る
GraphQLの仕様というよりgraphql-jsの実装の話。
GraphQLのビルトインのスカラー型はID、String、Int、Float、Booleanの5つだが、自分でスカラー型を作ることもできる。例えば日次を表すDateTime型とか。こんな感じ。
// schema.js
import {
  GraphQLScalarType,
  GraphQLObjectType,
  GraphQLSchema,
} from 'graphql';
import { Kind } from 'graphql/language';
let parseDate = (str) => {
  let d = new Date(str);
  return Number.isNaN(d.getTime()) ? null : d;
};
let DateTimeType = new GraphQLScalarType({
  name: 'DateTime',
  serialize: value => {
    return value.toJSON();
  },
  parseValue: value => {
    return parseDate(value);
  },
  parseLiteral: ast => {
    return ast.kind === Kind.STRING ? parseDate(ast.value) : null;
  },
});
DateTime型みたいなのは、文字列で渡ってきた日付のデータをパースしてアプリケーション内部ではDateとして扱い、レスポンスのJSONにするときにDateを文字列に変換して返したい。GraphQLScalarTypeの引数に設定している関数はそのための変換処理を行うもの。
parseValue、parseLiteralがリクエストのクエリからデータを受け取ってアプリケーション内部で利用するデータに変換し、serializeはレスポンスを返す前に適切なデータに変換するための関数を定義する。
このDateTime型を使って次のようなスキーマを定義する。
let ExampleType = new GraphQLObjectType({
  name: 'Example',
  fields: { created: { type: DateTimeType } },
});
let QueryType = new GraphQLObjectType({
  name: 'Query',
  fields: {
    example: {
      type: ExampleType,
      args: { date: { type: DateTimeType } },
      resolve: (_, args) => {
        // Dateで渡ってくる
        assert(args.date instanceof Date);
        // Dateとして何か処理して
        // Dateで返す
        return { created: args.date };
      },
    }
  }
});
let schema = new GraphQLSchema({ query: QueryType });
入力データとしてdateを受け取って、createdというフィールドで値をそのまま返すだけ。dateとcreatedはどっちもDateType型。このスキーマに対してこういうクエリを投げる。
{
  example(date: "2015-01-01T00:00:00Z") { created }
}
この場合はアプリケーションに値が渡される前にparseLiteralに"2015-01-01T00:00:00Z"という値が渡されて呼ばれる。また、このとき値だけでなく、GraphQLのASTデータが渡されて、クエリに指定されているデータ型なども一緒にチェックできる。
  parseLiteral: ast => {
    // ast.value === "2015-01-01T00:00:00Z"
    return ast.kind === Kind.STRING ? parseDate(ast.value) : null;
  },
ここでnullを返すと不正な値ということでエラーになる。もしくはエラーにするのにGraphQLErrorのインスタンスを返してもいいみたいだけど内部のScalar型の定義がnullを返しているのでそれに従っている。
graphql-js/scalars.js at 9234c6da0edbc4d2d2f3ff5d544a5980168d69ac · graphql/graphql-js
parseLiteralはこのようにクエリ内に直接DateTime型の値が埋め込まれたときに呼ばれるのに対して、variablesでDateTime型の値が指定された場合に呼ばれるのがparseValue。例えばこういうクエリ。
query foo($d: DateTime) {
  example(date: $d) { created }
}
このクエリのvariablesがこういう感じだとする。
{ "d": "2015-01-01T00:00:00Z" }
variablesで渡された値はASTを検査する必要はないのでparseValueには値のみが渡ってくる。
  parseValue: value => {
    // value === "2015-01-01T00:00:00Z"
    return parseDate(value);
  },
parseValueやparseLiteralで返した値はresolveの引数に渡される。ここ。
      resolve: (_, args) => {
        // Dateで渡ってくる
        assert(args.date instanceof Date);
        // Dateとして何か処理して
        // Dateで返す
        return { created: args.date };
      },
そしてこのresolveでDateTime型のデータを返した場合はそのデータがserializeに渡される。
  serialize: value => {
    return value.toJSON();
  },
このserializeで返した値がレスポンスとして返される。
let query = `
query foo($d: DateTime) {
  example(date: $d) { created }
}
`;
let variables = { d: "2015-01-01T00:00:00Z" };
graphql(schema, query, null, variables).then(result => {
  console.log(result);
  //=> { data: { example: { created: '2015-01-01T00:00:00.000Z' } } }
});
input/ouputともに文字列だけどアプリケーション内部(resolve内)ではDateで処理できるのがわかる。
コード全文はこちらに。
https://github.com/hokaccha/graphql-examples/tree/master/examples/datetimetype
- Prev Entry:GoでEnd To End Testingフレームワーク書いた
 
