Webtech Walker

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の引数に設定している関数はそのための変換処理を行うもの。

parseValueparseLiteralがリクエストのクエリからデータを受け取ってアプリケーション内部で利用するデータに変換し、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というフィールドで値をそのまま返すだけ。datecreatedはどっちも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);
  },

parseValueparseLiteralで返した値は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

このエントリーをはてなブックマークに追加