読者です 読者をやめる 読者になる 読者になる

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

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

GraphQLサーバー起動まで試した

Facebookの発表したGraphQLが気になったのでインストールまでやってみました。

qiitaではまだ記事が全然上がっていない模様。

GraphQLに関する1件の投稿 - Qiita

ここにすでにサーバーの実装がいたので試すのは簡単そう。

RisingStack/graphql-server · GitHub

インストールの説明に手順が書いてあるので問題ないのですが以下のような手順でやりました。 環境はUbuntu 14.04です。

sudo apt-get install mongodb
git clone https://github.com/RisingStack/graphql-server.git ~/graphql-server
cd ~/graphql-server
npm install
npm run seed
npm start

これでサーバーが起動します。mongodbが起動していないと、"npm run seed"のところで

Error: connect ECONNREFUSED
    at exports._errnoException (util.js:746:11)
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1010:19)

こんなエラーが出てしまうので注意。

無事サーバーが起動したところで、別のコンソールで

npm run client

とクライアントを起動すると何か出力が出て起動を確認できました。めでたしめでたし

次はもう少し弄ってみる予定。

普段どうやってエラーメッセージから問題を解決しているか?

そもそも

慣れれば簡単にぐぐれるだろ!って思いがちなんだけど、普段どうしているかちゃんと振り返っておかないと、人に伝えられないので振り返ってみます。

エラーに遭遇する

エラーは突然やってくる。ちょっと今流行りのモジュールを動かそうと思ってwikiやブログ記事の通りにコマンドを打ったのにも関わらずハマってしまうことがよくある。

今回は、bundle installをして必要なライブラリをセットアップしようと思ったらハマってしまったあんまりプログラミングが詳しくない人を想定しています。

bundle install

Fetching gem metadata from http://rubygems.org/...........
Fetching version metadata from http://rubygems.org/...
Fetching dependency metadata from http://rubygems.org/..
Resolving dependencies....
Installing rake 10.4.2
Installing addressable 2.3.8
Installing io-like 0.3.0
Installing archive-zip 0.7.0
Installing backports 3.6.4
Using bundler 1.10.3
Installing coderay 1.1.0
Installing daemons 1.2.2
with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    /Users/hogehoge/.rvm/rubies/ruby-2.0.0-p0/bin/ruby -r ./siteconf20150607-53401-6yipdt.rb extconf.rb
checking for rb_trap_immediate in ruby.h,rubysig.h... no
checking for rb_thread_blocking_region()... yes
checking for ruby/thread.h... yes
checking for rb_thread_call_without_gvl() in ruby/thread.h... yes
checking for inotify_init() in sys/inotify.h... no
checking for __NR_inotify_init in sys/syscall.h... no
checking for writev() in sys/uio.h... yes
checking for rb_thread_fd_select()... yes
checking for rb_fdset_t in ruby/intern.h... yes
checking for rb_wait_for_single_fd()... yes
checking for rb_enable_interrupt()... no
checking for rb_time_new()... yes
checking for sys/event.h... yes
checking for sys/queue.h... yes
checking for clock_gettime()... no
checking for gethrtime()... no
creating Makefile

make "DESTDIR=" clean

make "DESTDIR="
compiling binder.cpp
make: g++-4.2: No such file or directory
make: *** [binder.o] Error 1

make failed, exit code 2

Gem files will remain installed in /Users/hogehoge/.rvm/gems/ruby-2.0.0-p0/gems/eventmachine-1.0.7 for inspection.
Results logged to /Users/hogehoge/.rvm/gems/ruby-2.0.0-p0/extensions/x86_64-darwin-11/2.0.0/eventmachine-1.0.7/gem_make.out
An error occurred while installing eventmachine (1.0.7), and Bundler cannot
continue.
Make sure that `gem install eventmachine -v '1.0.7'` succeeds before bundling.

時間が無駄にかかるぅって気分になるんですが、冷静になって対処します。

まずはErrorって文字がある行を探します。

ターミナルの設定によっては赤く強調されているかもしれませんね。

Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
make: *** [binder.o] Error 1
An error occurred while installing eventmachine (1.0.7), and Bundler cannot
continue.

の3箇所が見つかります。とりあえず

  • gem native extensionってのがビルドエラーになった
  • よくわからないけど、なんかエラーが起きた
  • eventmachine (1.0.7)がインストールできなくてBundlerが中断された ことがわかりました。

ある程度rubyを知っていればスキップできますが、知らない場合は文章に含まれるキーワードっぽいものを探してぐぐります。 今は、rubyをいじっていてBundlerを使ってインストールをしようとしているので、

ruby eventmachine

などとぐぐって、これが何なのか概要を確認しておくと良い場合があります。

一旦、eventmachineというライブラリのインストールに失敗してしまったことがわかりました。 (native extensionとは何なのか不明ですが)

キーワードを適当に並べてぐぐってみて、同じエラーが出て困っていた人がいないか調べます。 今回はこんなキーワードにしました。

gem eventmachine native extension error

ぐぐったところ、検索結果1ページ分全部見て同じシチュエーションの記事が見つけられなかったので最初のエラー内容をもう少し見直します。

他の作戦を考える

だいたいエラーメッセージの前後に、原因の詳細が出ていることが多いので周辺を確認します。

make "DESTDIR=" clean

make "DESTDIR="
compiling binder.cpp
make: g++-4.2: No such file or directory
make: *** [binder.o] Error 1

make failed, exit code 2

が気になりました。makeとはなんでしょうか?とりあえずぐぐります。 たぶんMake: Japanではないでしょうw

wikipediaが見つかりました。その他の数ページを軽くみた限りではコマンド名であることがわかります。make - Wikipedia

普段コンパイルするような言語を触っていないとwikipediaに出てくる数々にキーワードが全然ピンと来なくて沼にハマりそうな気がしますが、あまり気にせず無視して進みましょう。

makeがコマンド名ということがわかりました。先ほどのmakeコマンドから出ているであろうメッセージは若干の英語力があれば、短い文章なので理解ができると思います。

make: g++-4.2: No such file or directory

何かファイルかフォルダがないようです。これはエラーの原因の可能性がありそうなメッセージですね。 g++4.2という新しいキーワードがまた出てきましたが、一旦無視してぐぐります。

make g++-4.2: No such file or directory

すると

http://blog.inouetakuya.info/entry/20120716/1342415386

というページを見つけました。インストールに失敗しているライブラリは違いますが同じmakeコマンドからのメッセージが出ているので試してみます。 先ほどのブログを上から読んで

gcc と g++ がインストールされているか確認

うん、確かにg++というのはあるぞ。

gcc 及び g++ へのシンボリックリンクを作成する

適宜シンボリックリンクが何か調べた方がいいですが、最悪とりあえずコピペでやってみるのもアリ。

g++4.2がないと怒られているので、これだけやれば良さそうです。

sudo ln -s /usr/bin/g++ /usr/bin/g++-4.2

改めてbundle installをしてみたところインストールが完了しました。めでたしめでたし

まとめ

この前ハマったのをモデルケースとして出来るだけ知識がないつもりで再度やってみました。

以下の点がポイントかなと個人的には思っています。

  • とりあえずエラーメッセージとそうでないものを仕分ける
  • エラーメッセージ周辺を読む(簡単な英語力で読めるケースが多い)
  • 概要だけを把握して、どういうキーワードでぐぐるべきかの判断力を磨く
  • とりあえずググる。だいたい個人ブログかstack overflowに回答がある

最初のうちは周りに詳しい人がいれば、サクッと聞いてしまうのが良いですね。これはこれで良い聞き方の話がありそうなので気が向いたら整理したいところです。

QNapにgitbucketをインストール

ググるとちょこちょこ試している人がいるようなので割と簡単にインストールできるみたい。実際に試してみた。

  1. AppCenterからJRE_ARM ver8.6.0、Tomcat ver7.0.50、git ver1.8.4.2-1をインストール
  2. Tomcatのページを開いて、gitbucketのwarファイルをデプロイ
    gitbucketの最新版はこちら

という手順で簡単インストールの予定だったが、いくつかハマってしまった。

ハマったところ

Tomcatを起動したら白い画面

インストールできたものの起動したら白い画面であの可愛くない猫が表示されなかった。
よく原因がわかってないが、TomcatのサービスをAppCenterから一度無効にして再度有効化したら治った。

ファイルサイズ制限に引っかかった

gitbucketをデプロイしようとしたところデプロイできるファイルサイズ制限に引っかかったのでsshnasに入って

/share/MD0_DATA/.qpkg/Tomcat/tomcat/webapps/manager/WEB-INF/web.xml
vimで以下のように編集してtomcatを再起動してやり直しました。

    <multipart-config>
      <!-- 60MB max -->
      <max-file-size>62914560</max-file-size>
      <max-request-size>62914560</max-request-size>
      <file-size-threshold>0</file-size-threshold>
    </multipart-config>

なんか起動しなくなった

gitbucketの設定ファイル は
/share/MD0_DATA/homes/admin/.gitbucket/gitbucket.conf
にあったのでインストールをやり直すときはこれをフォルダごと消してやり直してどうにかなった。

pogoplugにdebianインストールできました

手元に結局使わず転がっていたPogoplug Mobileを有効利用すべくDebianをインストールしました。2014/11/22時点では簡単にインストールができました

http://blog.qnology.com/2014/07/hacking-pogoplug-v4-series-4-and-mobile.html

の手順の通りに上から順にコマンドを打っていくだけです。
DebianをSDカードインストールすることを前提にしているのでUSBメモリからOSを起動したい場合はところどころコメントでUSBメモリの場合はこちらみたいに書かれているので確認しながらコマンドを打つと良いです。

root@debian:~# cat /etc/debian_version
7.4

 のようにDebian 7.4がインストールできました。eyefiのサーバーとか音楽サーバーに出来ないか色々試していきたいです。

hubotのインストールからアダプタ、スクリプトの自作まで

※2014/11/15 スクリプトの書き方加筆

オレオレシステムに投稿するためのボット作成にhubotを使ってみました。
公式ページの最新ぽいやり方でインストールを進めてみてハマったところをメモ。

インストール


hubot/README.md at master · github/hubot · GitHub

より

% npm install -g yo generator-hubot

 でyeomanというテンプレートからプロジェクト生成を行うツールとhubot用のテンプレートをインストール。

% mkdir myhubot
% cd myhubot
% yo hubot

適当にフォルダを作ってyoコマンドを使ってプロジェクト作成のウィザードを起動します。色々聞かれますが練習なら何も入力しなくていいはずです。
以下では全てmyhubotフォルダをカレントディレクトリに設定している前提です。

※既にhubotをインストールしていたらyo hubotのところでコケました

アダプタを作る

オレオレシステムに投稿するためのアダプタを作ります。投稿やタイムラインの受信を実装すればオレオレシステムにボットが動かせます。

node_modules/adapters/にアダプタのソースをおけばいいので

% cp node_modules/hubot/src/adapter.coffee node_modules/hubot/src/adapters/myadapter.coffee

を実行してテンプレートをコピー。
node_modules/hubot/src/robot.coffeeの

HUBOT_DEFAULT_ADAPTERS = [
  'campfire'
  'shell'
  'myadapter'
]
辺りを自作したアダプタ名を追加して保存する。

アダプタの実装方法はサンプルを見るかググった方が詳しいページが出てくるので割愛します。

% bin/hubot -a myadapter

で動作確認します。エラーが出なければ成功。

スクリプトを作る

スクリプトは実際のボットの動作を記述するもので、前述のアダプタのおかげで特定のシステム依存のコードを書く必要がないので使い回しが楽チン。
スクリプトにもテンプレートがあるのでそれを使えばOK。スクリプトはscriptsフォルダに配置したものが全て読み込まれるようです。
scripts/example.coffeeをコピーしてオレオレスクリプトを書き始めると良いでしょう。

割と公式の情報も充実しているので慣れてきたら読んでみようと思います。

hubot/adapters.md at master · github/hubot · GitHub

hubot/scripting.md at master · github/hubot · GitHub

 

実際に書き始めてみたら、self.emit 'connected'を実行しないとスクリプトの読み込みが進まなかったのでしばらくハマりました。
何かハマったら

export HUBOT_LOG_LEVEL="debug"

してから実行するとデバッグログが出るようになるので何かヒント見つかるかもしれません

絵を普段描かないから気づかなかったけど、絵描きも結構体系化されている知識があるんじゃないかと

前に熱中教室というイベントに行ってきて思ったんですが*1

車のデザイン画などは、右上がりになっている構図が多い印象があるのはデザイナーがみんな右利きなんじゃないかと。。。

熱中教室の山中先生の授業でデッサンするときに骨を意識して描きましょうというお話だったんですが、初級編として円を描く練習をやりました。
何周もクルクル描いてきれいな円になったらペンを抜きましょうという練習をしました。

正円はいいとして、斜めになっている円を描こうとしたら、どうも見本と同じように描けなくて苦労しました。
左上がりの円の方が描きやすく、右上がりの円が描きづらかったです。なので利き手によって描きやすさが異なるんだなと気づきました。
(似たような話をしている人もいますね http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q1386268378

そうつまり、骨(構造)の問題だ!

絵を描くに当たって描きやすいものと、描きにくいものがあるって発見をしたわけです。
こんなことを知り合いに話していたら、熱中教室で教えてもらったような”骨を意識して描こう”とか描くときのハウツーを学校とかで教えてもらえるらしいことがわかりました。
絵を描くのは見稽古しかない世界かと勘違いしてましたが、実は高速道路*2あるじゃん!と感激したわけです。
実は知らないだけで、絵描き界のRuby on Railsみたいなものが実はあったりするんじゃないかと。。。
そういうものがもっと溢れて世界総アマチュア時代にやっぱり突入するんだろうし、それを加速させたいなと思いました。

 

全然ジャンルの違う勉強会やイベントに行っても色々発見があって面白いですね。
それにしても山中先生=骨展*3のイメージだったので、デッサンで骨の話をしだしたときは納得でしたw

おまけ 円の書き方など


動画で解説! 指だけでキレイな円を描く方法 | ライフハッカー[日本版]

このハックは練習しておいてもいいかも。


World Freehand Circle Drawing Champion - YouTube

すげーきれいに一発で書いてて面白いです。体をちゃんと使ってるのがポイントかと。

*1:3331熱中教室 8/9,8/10 | Peatix

*2:将棋羽生さんの学習の高速道路論

*3:21_21 DESIGN SIGHT