GraphQLでDateTimeを扱う時、フォーマットを指定する。

GraphQLにはScalarTypeというクラスが存在する。

ScalarTypeは配列やオブジェクト(いわゆるkey-valueペア)、enumではないタイプの基底クラスになる。

GraphQLではこのScalarTypeをベースとした5つの基本タイプが仕様として定義されている。

  • Int - 符号付き32ビット整数
  • Float - 倍精度少数
  • String - UTF-8文字シーケンス
  • Boolean - true or false
  • ID - GUID

これだけだとDateTime等で困るので、DateTimeのGraphQLTypeを作ってみる。

date_time_type.rb

graphql-rubyを使う

https://github.com/rmosolgo/graphql-ruby

DateTimeType = GraphQL::ScalarType.define do
  name 'DateTimeType'
  description 'ActiveRecord::Type::DateTimeに対応したType'

end

このままだとクエリに対する出力結果が

{
  "data": {
    "series": {
      "created_at": "2017-01-09T18:55:30.000Z"
    }
  }
}

になる。せっかくGraphQLなので、フォーマットもクエリとして与えられるようにしたい。

created_atフィールドをいじる

普通にcreated_atを一属性として扱うだけなら

field :title, DateTimeType

の記述だけで済むが、今回は引数formatを与えると、その通りに整形してくれるようにする。

  field :created_at do
    type DateTimeType
    argument :format, types.String
    resolve ->(obj, args, ctx) {
      return obj.created_at if args[:format].nil?

      obj.created_at.strftime(args[:format])
    }
  end

これでqueryを叩く

{
  series(id: 3) {
    created_at(format:"%Y年%m月%d日 %H:%M:%S")
  }
}

{
  "data": {
    "series": {
      "created_at": "2017年01月09日 18:55:30"
    }
  }
}

良い。 本当はフォーマットの仕組みをTypeそのものに持たせたいのだけれど、TypeのresultがStringを取ることになるので適切では無さそう。

RailsのカスタムGeneratorを自分で作る

事前に用意したテンプレートを基にファイルを生成するようなコマンドを作る。

今回はGemにしたいのでプラグイン作成の想定でやる。

Railsプラグイン作成環境を用意

bin/rails plugin new sampleplugin

gemspecファイルのTODOになってるところを書き換えて、bundle installをする。

プラグイン自体はlibディレクトリ以下に作っていき、 test/dummyrailsプロジェクトがあるのでそこで動作確認をしていく。

Generatorクラスを作る

lib/generators/sample_generator.rb にgeneratorの処理を記述する

class SampleGenerator < Rails::Generators::Base

  def initialize(args, *options)
  super

  @_args, @_options = args, options
  end

  def main
    # ここに処理を書く  
  end
end

これでbin/rails generate sampleなどと叩くとmainが実行される。

テンプレートの用意

例えば

<%= @type_name %> = GraphQL::ObjectType.define do
  name '<%= @model_name %>'

end

lib/generators/templates/types.rbにこんな感じでファイルを作っておく。

テンプレートを使う

class SampleGenerator < Rails::Generators::Base
  source_root File.expand_path('../templates', __FILE__)

  def initialize(args, *options)
  super

  @type_name ='sample_type'
  @model_name = 'Sample'
  end

  def main
    template "types.rb", "app/graphql/types/#{@type_name}.rb"
  end
end

f:id:anoChick:20170118032812p:plain

できた。

Model(ActiveRecord)からGraphQL::ObjectTypesを自動生成する仕組みを考えるメモ

最近個人的にWebアプリ作ってます。 フロントはReactJS+Redux サーバサイドはRails それぞれ独立していて、GraphQLを用いて通信しています。

GraphQLのRuby実装として一番スターの多いgraphql-rubyを使っています。

GraphQL周りはまだ発展途上なのでいろいろと不便。 Typesを手動で定義するのが面倒なのでこれについて考えてみる。

Modelに対応するObjectの構成要素について

ActiveRecord内の要素で、今回着目するつもりなのは以下の通り - ActiveRecord::Attributes - ActiveRecord::Relation - ActiveRecord::Enum

ActiveRecordは GraphQL::ObjectTypeと対応させるとして, ActiveRecord::Relationの場合は has_oneの場合:GraphQL::ObjectType has_manyの場合:GraphQL::ListType(GraphQL::ObjectType) でよさそう。 ActiveRecord::EnumはそのままGraphQL::EnumTypeで問題ないはず。

ActiveRecord::AttributesはGraphQL::ScalarTypeになるのだけれど、 GraphQLの標準として定められているScalarTypeは

  • Int
  • Float
  • String
  • Boolean
  • ID

5つだけ。

- ActiveModel::Type::BigInteger
- ActiveModel::Type::Binary
- ActiveModel::Type::Boolean
- ActiveModel::Type::Decimal
- ActiveModel::Type::DecimalWithoutScale
- ActiveModel::Type::Float
- ActiveModel::Type::Integer
- ActiveModel::Type::String
- ActiveModel::Type::Text
- ActiveModel::Type::UnsignedInteger
- ActiveModel::Type::Value

Rails側はこんな感じ

対応できないGraphQL::ScalarTypeを新たに定義するのでもいいけど ユーザ独自で定義したActiveModel::Typeにも対応させることを考えると、 ActiveModel::Typeレベルから対応関係を結んだほうがよさそう。

StorybookでReactJSコンポーネントのレビュー環境を作る

f:id:anoChick:20161230123929p:plain

https://getstorybook.io/

StorybookとはWeb開発におけるUIコンポーネントを作る環境のこと。

インストール

まずは導入するReactJSプロジェクトを用意する。

mkdir react-sample
cd react-sample
npm init
npm i --save react react-dom

次にStorybookを導入するためのコマンド getstorybook をいれる。

npm i -g getstorybook
getstorybook
 getstorybook - the simplest way to add a storybook to your project.

 • Detecting project type. ✓
 • Adding storybook support to your "React" app. ✓
 • Preparing to install dependencies. ✓

yarn install v0.17.6
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 📃  Building fresh packages...
success Saved lockfile.
✨  Done in 21.14s.

 • Installing dependencies. ✓

To run your storybook, type:

   yarn run storybook

For more information visit: http://getstorybook.io

このようになったら導入成功。 yarnの利用が推奨されてるらしい。

storiesというディレクトリが作れていて、中に

  • Welcome.js
  • Button.js
  • index.js

3つのファイルが生成されている。

WelcomeとButtonはサンプルコンポーネントで,indexはコンポーネントをStorybookにつなぐためのもの。

使う

早速 yarn run storybookで実行してみる。

http://localhost:6006/にStorybookが表示される。 f:id:anoChick:20161230125638p:plain

import React from 'react';
import { storiesOf, action, linkTo } from '@kadira/storybook';
import Button from './Button';
import Welcome from './Welcome';

storiesOf('Welcome', module)
  .add('to Storybook', () => (
    <Welcome showApp={linkTo('Button')}/>
  ));

storiesOf('Button', module)
  .add('with text', () => (
    <Button onClick={action('clicked')}>Hello Button</Button>
  ))
  .add('with some emoji', () => (
    <Button onClick={action('clicked')}>😀 😎 👍 💯</Button>
  ));

storiesOfでストーリーを定義出来る。 コンポーネントの視覚的なテストのようなイメージ。 addでprops等を入れていく。 試しに small というpropertyがつくとボタンが小さくなるようにする。

index.js

.add('with small', () => (
    <Button onClick={action('clicked')} small>Small Button</Button>
  ))

を追加する。

import React from 'react';


export default class Button extends React.Component {

  constructor(props){
    super(props);
    this.styles = {
      border: '1px solid #eee',
      borderRadius: 3,
      backgroundColor: '#FFFFFF',
      cursor: 'pointer',
      fontSize: 15,
      padding: '3px 10px',
      margin: 10,
    };
  }

  render() {
    if(this.props.small){
      this.styles.fontSize = 10;
    }

    return (
      <button
        style={this.styles}
        onClick={this.props.onClick}
      >
        {this.props.children}
      </button>
    );
  }
}



Button.propTypes = {
  children: React.PropTypes.string.isRequired,
  onClick: React.PropTypes.func,
};

Buttonコンポーネントをこんな感じに書き換える。

f:id:anoChick:20161230155419p:plain

リアルタイムで見た目が変わった。

共有する

作成したStorybookプロジェクトはStorybookHUBで共有できる。

Storybook Hub - The Perfect Place To Develop & Review User Interfaces

まずStorybookにコメント機能を付ける。

// To get built in addons.
npm i -D @kadira/storybook-database-cloud
npm i -D @kadira/storybook-addon-comments

↑を実行した後`.storybook/addons.jsを記述する

import '@kadira/storybook/addons';

import '@kadira/storybook-database-cloud/register';
import '@kadira/storybook-addon-comments/register';

コミットしてGitHubにプッシュする。

Storybook Hub - The Perfect Place To Develop & Review User Interfaces StorybookHUBのサイトにAppを登録する。 いまプッシュしたGitHubリポジトリを対象にする。

f:id:anoChick:20161230171741p:plain

自動でビルドされて、StorybookのURLが生成される。 ↓こんな感じ

React Storybook

publicで公開することもできれば、privateでAppを作成して招待するという事もできる。

f:id:anoChick:20161230172150p:plain Storyに対してコメントが出来る。

ちなみにPublicリポジトリの場合は無料で利用できるけれど、 Privateリポジトリはコラボレータを増やすのにお金がかかる。

クローリングアプリを作る Part.1

今年得た知識を集めたらいい感じのクローリングアプリが作れそうな気がしたのでやってみる。

作りたい物

Architect

f:id:anoChick:20161229051026p:plain

こんな感じ。 非技術者でも簡単に利用できるような、 アプリ単体でも稼働するネイティブアプリケーション。

ポイントとしてはサーバとDBはちゃんと分離して、外に出せるようにしたい。 サーバは一般的なWebサーバとして実装しておけば外に出したとき接続先の変更だけで対応出来る。 DBは一工夫要りそう。

とりあえずは単体で完結するようなものを作る。

digdagを使う際に覚えておくべき事メモ

f:id:anoChick:20161223112454p:plain

タスクの状態が以下のいずれかになったときにタスクが開始される。

  • 依存するタスクが無い
  • 依存するタスクが全て完了している

実行のしかたは3つ

ローカル

digdag run piyo.dig

コマンドを叩いて実行

サーバ

digdag server -o digdag-server

サーバーが立つ http://127.0.0.1:65432 で管理画面

on Docker

docker:
  image: ubuntu:14.04

digファイルに↑のように記述されているとdockerで動く

バージョン管理

  • プロジェクトという単位でスクリプトやコンフィグを管理している。
  • リビジョンという単位でバージョンを管理している
    • サーバはアップロードされた過去のリビジョンを保持している

タスクとオペレータ

+piyoとかはタスク

yaki>: tori とかでアクションが実行できる

AWS上にサーバレスな汎用クローラを展開するぞ。

サーバレスな汎用スクレイパーを作った。 - あのにのに

前回はAPIGatewayとLambdaで、指定したURLの指定した位置にあるデータを抜き出すAPIを作った。

今回はサイト内探索をするようなシステムをAWS上に構築しようと思う

注意:クローラは用法用量を守って、相手方のサイトに迷惑がかからないように十分な配慮を徹底しましょう。

今回作るもの

f:id:anoChick:20161211213223p:plain

こんな感じの構成をイメージしてる。

DynamoDBの1レコードがサイトへの1リクエストに常に対応するものとし、内部リンクのURLを新たにDynamoDBのテーブルに追加していく。

得られたデータは対応レコードに格納される。

URLがテーブルに追加されると、DynamoDB Streamsに流され、Lambdaで実行される。

というような繰り返し。

DynamoDBのデータスキーム

1.Request先URL :target_url

例) https://example.com/items/1

2.巡回対象サイトのrootURL :root_url

例) https://example.com/

3.対象データのセレクタ :selector

例) .score

4.対象データのスクレイピング結果 :result

例) 4.2

f:id:anoChick:20161211214901p:plain

プライマリキーをtarget_urlに設定。

f:id:anoChick:20161211215707p:plain

ストリームに流れるのは"新しいイメージ"のみで良いはず。

SERVERLESS FRAMEWORKの用意

SERVERLESS FRAMEWORKが最高すぎる - あのにのに SERVERLESS FRAMEWORKを使う。 DynemoDBへのレコード追加時に、Lambdaに渡されるJSONのサンプルがほしいので一旦デプロイして動かしてみる。

functions:
  spide:
    handler: handler.spide
    events:
      - stream:
          arn: ####
          batchSize: 1
          startingPosition: LATEST

とりあえずデータを取得(スクレイピング

console.log(event);

ってやるとCloudWatchの方に吐き出されるので、そのデータをevent.jsonに入れてローカルで動かしてみる。

'use strict';
var client = require('cheerio-httpcli');

module.exports.spide = (event, context, callback) => {
  var url = event.Records[0].dynamodb.NewImage.target_url['S'];
  var selector = event.Records[0].dynamodb.NewImage.selector['S'];
  client.fetch(url, {}, function(err, $, res) {
    console.log($(selector).text());
  });
};

これで目的のデータは得られた。

結果をDynamoDBに格納

DynamoDBに入れておく。 DynamoDBを操作するために

GitHub - noradaiko/vogels: DynamoDB data mapper for node.js

をいれる。

var vogels = require('vogels'),
  Joi = require('joi');
var BasicSpider = vogels.define('BasicSpider', {
  hashKey: 'target_url',
  timestamps: true,
  schema: {
    target_url: Joi.string(),
    root_url: Joi.string(),
    selector: Joi.string(),
    result: Joi.string(),
  },
  tableName: 'BasicSpider'
});
BasicSpider.update({
  target_url: url,
  result: $(selector).text()
}, function(err, acc) {
  console.log('update account', acc);
});

でresultが追加されるはず。 ただ、今の状態だとUPDATEでもリクエストが飛んでしまうため、レコード追加時のみスクレイピング処理が走るようにする。

  if (!newImage.target_url || !newImage.selector || !newImage.root_url ||
    newImage.result || newImage.updatedAt)
    return;

一旦デプロイして動かしてみる。

f:id:anoChick:20161211233459p:plain

レコードを追加してちょっと待つと...

f:id:anoChick:20161211233520p:plain

入った!!!

DynamoDBに対象URLと対象要素のセレクタを入れると、resultにデータを保存する仕組み

が出来たことにななる。

これはこれでなんか良さそう。

内部リンクを巡る

過去に訪問したページ → UPDATE処理になる為実行されない。(無限ループはない)

対象データが存在しないページ → resultのないレコードとして残る。

とりあえず内部リンクを拾うって訪問したことのないページをレコード追加する。

$('a').each(function(i, e) {
  var link = ($(e).attr('href') || '').replace('rootURL','');
  if (link[0] == '/') {
    var target_url = rootURL + link;
    BasicSpider
      .query(
        target_url)
      .exec(function(err, acc) {

        if (acc.Count) return;

        BasicSpider.create({
          target_url: target_url,
          root_url: rootURL,
          selector: selector
        }, function(err, post) {
          console.log("create:" + target_url);
        });
      });
  }
});

後は諸々調整、timeoutとかmemorysizeとか。

実行

f:id:anoChick:20161212015117p:plain

取れてる。

ただ並列数がどうなるのかが気になる。 あまり同時に叩きすぎると対象サイトに迷惑がかかるのでコントロールしたい。

つづく