Life, Education, Death

プログラミング以外でも思ったことをつらつらと書きたい

GraphQLのサンプルに書き加えてみた

できたこと

独自のItemというデータ構造を追加してそれを作成・取得することができるようになった。

データ定義をしておくとバリテーションをしてくれることがわかった。 クライアント側で欲しいフィールドを調整したい場合は便利そうな気がした。

独自実装を追加していく

前回起動まで成功した https://github.com/RisingStack/graphql-serverをローカルにクローンしてきてこのクライアントとサーバーをいじっていきます。

Nodejs+MongoDBという環境で実装されていて、今回は

  1. mongoose(MongoDBライブラリ)のモデルクラスの定義
  2. GraphQLのスキーマを定義
  3. クライアント側コードを記述。作成と取得クエリを投げるようにする

という手順で作業を進めます。

mongoose(MongoDBライブラリ)のモデルクラスの定義

src/server/user.jsがmongooseのモデルクラスの定義なので、これをコピペしてsrc/server/item.jsを作ります。以下のようなコードになりました。

import mongoose from 'mongoose';

var ItemSchema = new mongoose.Schema({
  name: {
    type: String
  }
});

var Item = mongoose.model('Item', ItemSchema);

export default Item;
GraphQLのスキーマを定義

src/server/schema.jsを編集していきます。

import Item from './item';

を追加して先ほど書いたitem.jsを読み込みます。

GraphQLスキーマ向けにモデル定義をします。mongooseの方でもしてるので一緒にできると良いなと思います。

var itemType = new GraphQLObjectType({
  name: 'Item',
  description: 'Item creator',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'The id of the item.',
    },
    name: {
      type: GraphQLString,
      description: 'The name of the item.',
    },
  })
});

GraphQLSchemaのqueryには取得クエリ、mutationには作成・更新・削除などの変更を加えるクエリを実装します。 詳しくは仕様を読むと良いです。

取得クエリ(getItem)を定義

  query: new GraphQLObjectType({
    name: 'RootQueryType',
    fields: {
      getItem: {
        type: itemType,
        args: {
          name: {
            name: 'name',
            type: new GraphQLNonNull(GraphQLString)
          }
        },
        resolve: (root, {name}, source, fieldASTs) => {
          return Item.findOne({name: name});
        }
      },
    }
  }

次に作成クエリ(createItem)を定義します。

  mutation: new GraphQLObjectType({
    name: 'Mutation',
    fields: {
      createItem: {
        type: itemType,
        args: {
          name: {
            name: 'name',
            type: GraphQLString
          }
        },
        resolve: (obj, {name}, source, fieldASTs) => co(function *() {
          var item = new Item();
          item.name = name;

          return yield item.save();
        })
      },
    }
  }),

最終的にsrc/server/schema.jsは以下のようになりました。

import {
  GraphQLObjectType,
  GraphQLNonNull,
  GraphQLSchema,
  GraphQLString,
  GraphQLList
} from 'graphql/type';

import co from 'co';
import User from './user';
import Item from './item';

/**
 * generate projection object for mongoose
 * @param  {Object} fieldASTs
 * @return {Project}
 */
function getProjection (fieldASTs) {
  return fieldASTs.selectionSet.selections.reduce((projections, selection) => {
    projections[selection.name.value] = 1;

    return projections;
  }, {});
}

var userType = new GraphQLObjectType({
  name: 'User',
  description: 'User creator',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'The id of the user.',
    },
    name: {
      type: GraphQLString,
      description: 'The name of the user.',
    },
    friends: {
      type: new GraphQLList(userType),
      description: 'The friends of the user, or an empty list if they have none.',
      resolve: (user, params, source, fieldASTs) => {
        var projections = getProjection(fieldASTs);
        return User.find({
          _id: {
            // to make it easily testable
            $in: user.friends.map((id) => id.toString())
          }
        }, projections);
      },
    }
  })
});

var itemType = new GraphQLObjectType({
  name: 'Item',
  description: 'Item creator',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'The id of the item.',
    },
    name: {
      type: GraphQLString,
      description: 'The name of the item.',
    },
  })
});

var schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'RootQueryType',
    fields: {
      hello: {
        type: GraphQLString,
        resolve: function() {
          return 'world';
        }
      },
      user: {
        type: userType,
        args: {
          id: {
            name: 'id',
            type: new GraphQLNonNull(GraphQLString)
          }
        },
        resolve: (root, {id}, source, fieldASTs) => {
          var projections = getProjection(fieldASTs);
          return User.findById(id, projections);
        }
      },

      getItem: {
        type: itemType,
        args: {
          name: {
            name: 'name',
            type: new GraphQLNonNull(GraphQLString)
          }
        },
        resolve: (root, {name}, source, fieldASTs) => {
          return Item.findOne({name: name});
        }
      },
   }
  }),

  // mutation
  mutation: new GraphQLObjectType({
    name: 'Mutation',
    fields: {
      createUser: {
        type: userType,
        args: {
          name: {
            name: 'name',
            type: GraphQLString
          }
        },
        resolve: (obj, {name}, source, fieldASTs) => co(function *() {
          var projections = getProjection(fieldASTs);

          var user = new User();
          user.name = name;


          return yield user.save();
        })
      },
      deleteUser: {
        type: userType,
        args: {
          id: {
            name: 'id',
            type: new GraphQLNonNull(GraphQLString)
          }
        },
        resolve: (obj, {id}, source, fieldASTs) => co(function *() {
          var projections = getProjection(fieldASTs);
          console.log(id);
          return yield User.findOneAndRemove({_id: id});
        })
      },
      updateUser: {
        type: userType,
        args: {
          id: {
            name: 'id',
            type: new GraphQLNonNull(GraphQLString)
          },
          name: {
            name: 'name',
            type: GraphQLString
          }
        },
        resolve: (obj, {id, name}, source, fieldASTs) => co(function *() {
          var projections = getProjection(fieldASTs);

          yield User.update({
            _id: id
          }, {
            $set: {
              name: name
            }
          });

          return yield User.findById(id, projections);
        })
      },

      createItem: {
        type: itemType,
        args: {
          name: {
            name: 'name',
            type: GraphQLString
          }
        },
        resolve: (obj, {name}, source, fieldASTs) => co(function *() {
          var projections = getProjection(fieldASTs);

          var item = new Item();
          item.name = name;

          return yield item.save();
        })
      },
    }
  }),
});

export var getProjection;
export default schema;
クライアント側コードを記述。作成と取得クエリを投げるようにする

作成クエリを投げるコードを書くのでsrc/client/mutation.jsを編集します。

Itemデータを作成するために先ほど実装したcreateItemを使います。hogehogehogeというItemを作成します。

var itemName = 'hogehogehoge'

request
  .post('http://localhost:3000/data')
  .send({
    query: `
    mutation M($name: String!) {
      createItem(name: $name) {
        name
      }
    }
    `,
    params: {
      name: itemName
    }
  })
  .end(function (err, res) {
    debug(err || res.body);
    debug('created', res.body);
  });

src/client/mutation.jsは以下のようになりました。最初からあったコードはデバッグの都合で除去しました。

import request from 'superagent';
import Debug from 'debug';

var debug = new Debug('client:mutation');
var itemName = 'hogehogehoge'

request
  .post('http://localhost:3000/data')
  .send({
    query: `
    mutation M($name: String!) {
      createItem(name: $name) {
        name
      }
    }
    `,
    params: {
      name: itemName
    }
  })
  .end(function (err, res) {
    debug(err || res.body);
    debug('created', res.body);
  });

取得クエリを投げるコードを書きます。src/client/query.jsを以下のようなコードを追加します。 (var name = 'hogehogehoge'としていたらうまく動かなかったです。変数名とフィールド名がぶつかるとまずい?)

var itemName = 'hogehogehoge'
request
  .get('http://localhost:3000/data')
  .query({
    query: `{
      getItem(name: "${itemName}") {
        name
      }
    }`
  })
  .end(function (err, res) {
    debug('items', res.body);
  });

ということで、src/client/query.jsは以下のようになりました。最初からあったコードはデバッグの都合で除去しました。

import request from 'superagent';
import Debug from 'debug';

var debug = new Debug('client:query');
var itemName = 'hogehogehoge'

request
  .get('http://localhost:3000/data')
  .query({
    query: `{
      getItem(name: "${itemName}") {
        name
      }
    }`
  })
  .end(function (err, res) {
    debug('items', res.body);
  });

上記のようにコードを編集していったら、前回同様に

npm start

としてサーバーを起動して、別のscreenで

npm run client

とクライアントを起動すると、無事データの作成と取得が確認できます。