GraphQL Subscriptionsをgraphql-rubyとAction Cableで作る

graphql-rubyでは、RailsのAction Cableに乗ることでGraphQL Subscriptionsを実装できます。

GraphQL Subscriptionsとは

GraphQL Subscriptionsは、あらかじめ特定のGraphQLクエリを購読しておき、サーバ側でイベントが起きるたびにその形式のデータを受信できる仕組みです。用途としてはプッシュ通知などを想定しているようです。

2018年4月の段階ではまだworking draftですが、FacebookによるGraphQL SubscriptionsのRFCがあります。ここではアーキテクチャだけが示されており、具体的な実装方法については言及していません。

github.com

Ruby/RailsまわりのGraphQL Subscriptionsの実装としては、graphql-rubyが提供しているものがあります。バックエンドとしては

  • RailsのAction Cable
  • Pub/SubメッセージングサービスのPusher

をサポートしており、Pusherのほうは有料のpro版の機能となるので、最初はAction Cableを選択することになると思います。この記事でもAction Cableを使います。

GraphQL Subscriptionsを実装する

Order(注文)が入ったときにクライアントへ通知するサーバをGraphQL Subscriptionsとして実装します。

この記事では次のgemを使います。

  • rails 5.1.6
  • graphql-ruby 1.7.14

次の手順で実装を進めます。

  • 配信するデータを準備する
  • Action Cableを準備する
  • Subscription Typeを追加する
  • GraphqlChannelを追加する
  • データを配信する

配信するデータを準備する

事前にAPIモードでプロジェクトを作っておき、bin/rails g scaffold order price:integerを実行して、OrderをCRUDできる状態にしているものとします。また、OrderTypeを次のように定義しておきます。

Types::OrderType = Graphql::ObjectType.define do
  name 'Order'
  field :price, !types.Int
end

Action Cableを準備する

以前、Action CableでWebSocketのAPIを作って通信する方法について書いたので、こちらを見てください。Action Cable特有のメッセージ形式についても説明しています。

blog.kymmt.com

WebSocketの接続確立時に作られるApplicationCable::Connectionに、subscribeするユーザの認証処理を入れます。

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user || reject_unauthorized_connection
    end

    private

    def find_verified_user
      User.first
    end
  end
end

今回はfind_verified_userで手を抜いて、必ずUser.firstが接続してきているような実装にしていますが、実際はここでなんらかの認証が走るようにします。

Subscription Typeを追加する

スキーマ定義にSubscriptionTypeのフィールドを追加します。また、GraphQL::Subscriptions::ActionCableSubscriptionsをスキーマ定義でuseすることで、スキーマでSubscriptionsを使えるようにします。

AppSchema = GraphQL::Schema.define do
  subscription Types::SubscriptionType
  use GraphQL::Subscriptions::ActionCableSubscriptions
end

クライアントが受信するデータを表すSubscriptionTypeを書きます。

Types::SubscriptionType = GraphQL::ObjectType.define do
  name 'Subscription'

  field :orderCreated do
    subscription_scope :current_user_id
    type Types::OrderType
  end
end

あとで述べるGraphqlChannelに対して購読メッセージを送ると、サーバ側でイベントが発生したときに、ここで定義した型でデータが配信されます。また、subscription_scope :current_user_idを指定することで、あとで説明するtrigger利用時にcurrent_userにだけデータを配信できます。

GraphqlChannelを追加する

次に、GraphQL Subscriptionsを使うときにAction Cableのクライアントが購読するGraphqlChannelを定義します。graphql-rubyのAPIドキュメントにサンプル実装が載っています。

Class: GraphQL::Subscriptions::ActionCableSubscriptions — Documentation for graphql (1.7.14)

抜粋します。

class GraphqlChannel < ApplicationCable::Channel
  def subscribed
    @subscription_ids = []
  end

  def execute(data)
    query = data["query"]
    variables = ensure_hash(data["variables"])
    operation_name = data["operationName"]
    context = {
      current_user: current_user,
      # Make sure the channel is in the context
      channel: self,
    }

    result = MySchema.execute({
      query: query,
      context: context,
      variables: variables,
      operation_name: operation_name
    })

    payload = {
      result: result.subscription? ? nil : result.to_h,
      more: result.subscription?,
    }

    # Track the subscription here so we can remove it
    # on unsubscribe.
    if result.context[:subscription_id]
      @subscription_ids << context[:subscription_id]
    end

    transmit(payload)
  end

  def unsubscribed
    @subscription_ids.each { |sid|
      CardsSchema.subscriptions.delete_subscription(sid)
    }
  end
end

これを使えばだいたい動きますが、なにをやっている実装なのかいろいろ気になるので、メソッドごとに見ていきます。

subscribed

ApplicationCable::Channelを継承すると、クライアントからこのチャンネルへsubscribeメッセージが送られたときにsubscribedメソッドが実行されます。Rails Guidesの説明などでは、ここでstream_fromを使ってストリームを自前で管理するように書いてありますが、graphql-rubyでは、このあと説明するexecute実行時にgem側でストリームを管理するので、ここでストリームを管理する必要はありません。

また、execute/unsubscribedで使うために@subscription_idsを空配列として定義しています。

execute

クライアントからこのチャンネルへexecuteを実行するメッセージが送られたときにexecuteメソッドが実行されます。ここはgraphql-rubyを使っているときによく見るGraphqlController#executeの実装とよく似ています。

GraphQL::Schema#executeを実行しているので、subscribe後に普通のqueryやmutationのクエリを送ると、それが実行されて結果を返すことができます。一方、subscriptionクエリを送った場合は、executeすると次の場所でsubscription IDと対応するGraphQLクエリを登録し、stream_fromでストリームを開きます。

graphql-ruby/action_cable_subscriptions.rb at v1.7.14 · rmosolgo/graphql-ruby

このGraphQL::Subscriptions::ActionCableSubscription#write_subscriptionはsubscriptionクエリを受信後にGraphQL::Subscriptions::Instrumentation#after_queryで実行されます。

graphql-ruby/instrumentation.rb at v1.7.14 · rmosolgo/graphql-ruby

この結果、result.subscription?trueになります。このときは、まだイベントが発生したわけではないので、payloadとして空の結果を返します。また、unsubscribed用にsubscription IDを@subscription_idsに入れておいて、最後にAction CableのActionCable::Channel::Base#transmitでクライアントへデータを送信します。

なお、例のコードはpayloadresultnilですが、本当は{ data: nil }が正しいです*1

unsubscribed

クライアントからクエリへunsubscribeメッセージが送られたときに実行されます。execute時に登録されたsubscription IDに対応するsubscriptionをdelete_subscriptionで削除しています。delete_subscriptionは次のコードです。

graphql-ruby/action_cable_subscriptions.rb at v1.7.14 · rmosolgo/graphql-ruby

登録したクエリを削除することで、write_subscriptionを実行したときにデータが配信されなくなります。

データを配信する

subscriptionを登録したクライアントに対するデータの配信にはGraphQL::Subscriptions#triggerを使います。今回はわかりやすくPOST /ordersを叩いたときに、クライアントへデータを配信してみます。

class OrdersController < ApplicationController
  def create
    @order = Order.new(price: params[:price])
    if @order.save
      AppSchema.subscriptions.trigger('orderCreated', {}, @order, scope: @order.user_id)
      render status: :created
    else
      # ...
    end
  end
end

triggerの詳しい説明は公式ガイドにあります。

GraphQL - Triggers

動作を確認する

WebSocket経由でサーバへメッセージを送信することで、購読とデータの受信ができるか確かめます。

リクエスト手順

サーバへは次のGraphQLクエリを送ります。

subscription {
  orderCreated {
    price
  }
}

bin/rails sしてサーバを立ち上げたあとに、wscat -c localhost:3000/cable'でWebSocketへ接続後、次の順番でデータを送ります。

  • {"command":"subscribe","identifier":"{\"channel\":\"GraphqlChannel\"}"}
    • GraphqlChannelの購読
  • {"command":"message","identifier":"{\"channel\":\"GraphqlChannel\"}","data":"{\"action\":\"execute\",\"query\":\"subscription{orderCreated{price}}\"}"}
    • GraphqlChannel#executeを実行して指定クエリを購読

このあと、適当なHTTPクライアントでPOST /ordersへ次のJSONを投げます。

{"order":{"price":1000}}

これで、OrdersController#createでtriggerが実行されて、subscriptionクエリで購読した構造のデータをwscatが次のように受信するのが見えるはずです。

< {"identifier":"{\"channel\":\"GraphqlChannel\",\"channelId\":\"16280cac9e0\"}","message":{"result":{"data":{"orderCreated":{"price":"1000"}}},"more":true}}

実際は、mutationでデータを変更したときにtriggerしたり、バックグラウンドジョブが実行完了したときにtriggerするとそれっぽくてよいと思います。

クライアントについて

今回はサーバ側だけ作ったのでwscatで動くかどうか確認しました。アプリケーションを作るときは、Subscriptionsに対応したモジュールがApolloやRelayに存在するので、これらを使えばJSでSubscriptions対応のクライアントを作ることができます。

wscatでAction Cableと通信する

Railsで/cableなどのエンドポイントにAction CableをマウントするとWebSocketサーバとして利用できます。wscatを使ってAction CableによるWebSocket APIと対話的に通信するために、送信するデータの形式などを調べました。

準備

wscat

github.com

npmでインストールできます。

$ npm install -g wscat

Action Cable

この記事ではRails 5.1.6を使います。今回は、APIモードのRailsアプリケーションにAction Cableをマウントします(Action Cableサーバを独立に起動することも可能)。まず、適当にアプリを作ります。

$ rails new --api action-cable-sample
$ cd action-cable-sample
$ bin/rails g scaffold message body:string
$ bin/rails db:migrate

config/application.rbaction_cable/engineを読み込み、さらにマウントパスを指定します。/cableにマウントするのがRails wayの様子なのでそうします。

# config/application.rb
require_relative 'boot'

require "rails"
# ...
require "action_cable/engine"
# ...

module ActionCableSample
  class Application < Rails::Application
    # ...
    config.api_only = true

    config.action_cable.mount_path = '/cable'
  end
end

また、デフォルトではCSRF対策で同じオリジンからしかWebSocket通信できないので、開発環境ではどのオリジンからでもWebSocket通信できるように設定します。

# config/environments/development.rb
Rails.application.configure do
  # ...
  config.action_cable.disable_request_forgery_protection = true
end

あとはAction Cableのチャンネルを適当に作ります。

$ bin/rails g channel message

MessageChannelは次のように書いておきます。

class MessageChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'message_channel'
  end

  def unsubscribed
  end
end

また、今回はmessages#createが成功したときにWebSocket経由でメッセージをブロードキャストします。

class MessagesController < ApplicationController
  # ...

  def create
    @message = Message.new(message_params)

    if @message.save
      ActionCable.server.broadcast 'message_channel', body: @message.body

      render json: @message, status: :created, location: @message
    else
      render json: @message.errors, status: :unprocessable_entity
    end
  end
end

実際に通信する

次のコマンドでWebSocketサーバへ接続します。HTTPでリクエストしてからWebSocketへアップグレードする処理などは自動でやってくれます。

$ wscat -c localhost:3000/cable

connected (press CTRL+C to quit)

< {"type":"welcome"}
>

Action Cableへ送信するデータにはsubscribe, message, unsubscribeの3種類があります。次の形式でデータを送信することでAction Cableとやりとりできます。

{"command":"subscribe","identifier":"{\"channel\":\"MessageChannel\"}"}
{"command":"message","identifier":"{\"channel\":\"MessageChannel\"","data":"\"action\":\"chat\"}"} # "chat"は例です
{"command":"unsubscribe","identifier":"{\"channel\":\"MessageChannel\"}"}

Action CableはフルスタックアプリでJSを書いて使うことを想定されているためか、このあたりの仕様はREADMEやRails Guidesを見てもとくにドキュメント化されていないようでした。仕様を把握するにはAction Cableのコードを読む必要があります。

ActionCable::Connection::Subscriptions#execute_commandで受信したデータを解析し、commandに指定された文字列subscribe, message, unsubscribeによって処理を分岐しています。messageを送信したときはActionCable::Channel::Base#perform_actionに移り、受信データのactionで指定された名前を持つチャンネルのメソッドを動的に呼び出しています。

また、キー"identifier"の値が文字列化されたJSONになっているのは、この文字列がActionCable::Connection::Subscriptionsの中でActiveSupport::JSON.decodeに渡るからです。

実際に上述した形式のsubscribeのデータを送ると、チャンネルを購読できます。

> {"command":"subscribe","identifier":"{\"channel\":\"MessageChannel\"}"}
< {"identifier":"{\"channel\":\"MessageChannel\"}","type":"confirm_subscription"}

その後、コントローラのアクション内からブロードキャストするとメッセージを受信できます。

# curlで叩く
$ curl --request POST --url http://localhost:3000/messages --header 'content-type: application/json' --data '{"message":{"body":"test"}}'

# wscatでデータを受信する
< {"identifier":"{\"channel\":\"MessageChannel\"}","message":{"body":"test"}}

参考

GraphQL APIを作るときにテストをどう書いていくか

こういうのはどうかという最近の考えを書いておきます。とはいっても、だいたいはgraphql-rubyのドキュメントに書いてあります。Rails + graphql-ruby + RSpecが前提です。

各フィールドのテスト

フィールドから正しく値を取得できるか、つまりRailsのモデルとgraphql-rubyとの連携が正しいかという点をテストします。次のようにスキーマのオブジェクトから型情報を取得して、resolveを実行します。

# たとえば spec/graphql/types/user_type_spec.rb に書く
user_type = MySchema.types['User']
user = FactoryBot.create(:user, name: 'Foo')
expect(user_type.fields['name'].resolve(user, nil, nil)).to eq 'Foo' # nameフィールドが正しい値を取得できるかのテスト

resolverの処理を切り出す

resolveを明示的に指定していて、さらに少し複雑な場合があると思います。

Types::ArticleType = GraphQL::ObjectType.define do
  # ...
  field :something do
    type !types.String
    resolve ->(article, args, ctx) {
      # ref: https://github.com/Shopify/graphql-batch/blob/master/examples/association_loader.rb
      Loaders::AssociationLoader.for(Article, :comments).load(article).then do |comments|
        # articleとcommentsからなにかを生成する処理
      end
    }
  end
end

この場合、resolverの処理をクラスとして外に出し、そのクラスをテストすると見通しが少しよくなります。

これに関連して、graphql-rubyのドキュメントにおける"Testing"のページの"Don't test the schema"という節を見ると、だいたい次のようなことが書いてあります。

  • スキーマはテストしない
  • フィールドのテストはresolverの処理を切り出してapp/models配下に単機能のクラスとして置く
    • #new#valueだけ持つクラス
    • #newでフィールドが属する型に対応するオブジェクトを受け取る
    • #valueでresolverの中にもともとベタ書きされていた処理を実行する
  • GraphQLのスキーマとフィールド内の処理が疎になって便利

これはapp/models配下にGraphQLだけで使いそうなクラスが混ざる点が気になるので、app/models配下には置かずapp/graphql/types/<型名>_fields配下にフィールドごとにresolverを切り出します。

app
└ graphql
  └ types
    └ <型名>_fields
      └ <フィールド名>.rb

<型名>_fields配下のクラスは上述したgraphql-rubyのドキュメントにおけるものと同じような、#valueだけをメソッドとして持つ単機能のクラスです。

ちょっと適当な例ですが、このようなクラスを使うとフィールドの定義は次のようになります。

Types::ArticleType = GraphQL::ObjectType.define do
  # ...
  field :something do
    type !types.String
    resolve ->(article, args, ctx) {
      Loaders::AssociationLoader.for(Article, :comments).load(article).then do |comments|
        Types::ArticleFields::Something.new(article, comments).value
      end
    }
  end
end

request specになにを書くか

request specでテストを書くと、スキーマが大きく/深くなるにしたがって、テスト中のクエリが大きくなり、さらに複数の種類を持つようになります。すると、必要な事前処理や期待値の準備が大変になり、メンテしづらくなります。

基本的には、request specにはGraphQL APIへのリクエストを受けるコントローラ内でのエラーケースに関するテストを書く、ぐらいがいいかと思います。たとえば認可されていないアクセスに対して401 Not Authorizedを返す処理をコントローラレベルでやっている場合、そのレスポンスをテストする、などです。

GraphQL APIをスキーマファースト開発するためのモックサーバをRailsとApolloで作る

GMOペパボ Advent Calendar 2017の23日目の記事です。

今回はJavaScriptでGraphQLのサーバ/クライアントや関連ツールを提供しているApolloのツールセットでRailsプロジェクトでGraphQLのモックサーバを立ち上げるところまでを試してみます。

業務でRails製の(RESTishな)Web APIとVue.js製のSPAからなるアプリケーションを開発していて、スキーマファースト開発を取り入れています。また、GraphQLで通信するAPIを実験的に導入しはじめていますが、こちらは明示的な開発フローを決めず導入しようとしているため、なかなかサクサクと開発が進まないのが現状です。そこで、GraphQLでも先にインタフェースだけを決めてから、モックサーバを使ってフロントエンドとバックエンドで並行開発していけばよいのでは、という発想になります。

しかし、そもそもGraphQLはサーバに対するクエリを書くためのスキーマありきの技術であり、それがRESTの文脈におけるAPIとは異なる点です。その点で、スキーマファースト開発と呼ぶと語弊があるかもしれません。ですが、ここでは「GraphQLの型やフィールドだけを書いて、実際にデータを問い合わせる部分(リゾルバ)を書かない」ことをスキーマファースト開発とひとまず呼びます。つまり、裏の実装を後回しにして、フロントエンド/バックエンドでインタフェースについて合意が取れればモックサーバを使って開発を進められる、という状態を目指します。

利用ツール

上述したとおり、Apolloのツールセットを使います。

www.apollographql.com

具体的には次のものを使います。

ダミーデータを返してくれるサーバのことをスタブサーバと呼んだりもしますが、graphql-toolsが"Mocking"という言葉を使っているので、この記事ではモックサーバと呼ぶことにします。

最終構成

今回はRailsでgraphql-rubyを使っている状況を想定します。Railsプロジェクトにおける最終的な構成は次のとおりです(関係する部分だけ書いています)。

.
├── app
│    └── graphql
│         ├── app_schema.rb
│         ├── mutations
│         └── types
├── lib
│    └── tasks
│         └── graphql.rake
└── mock_app
     ├── index.js
     ├── mocks.js
     ├── package.json
     └── type_defs.js

詳細は次の通りです。

  • app 配下にgraphql-rubyで書いたGraphQLスキーマを置く
  • lib 配下にGraphQLスキーマをダンプするRakeタスクを置く
  • mock_app 配下にモックサーバの実装を置く
    • type_defs.js はRakeタスクで生成する

想定する開発フロー

想定する開発フローは次の通りです。

  1. graphql-rubyのDSLでGraphQLの型やフィールドを書く
  2. 追加した型やフィールドのダミーデータを書く
    • モックサーバで使う
  3. レビュー
  4. モックサーバを立ち上げる
    • graphql-rubyでGraphQLスキーマをダンプしてApolloで使える形式にする
    • ExpressとApolloでGraphQLモックサーバを立ち上げる
  5. フロントエンド/バックエンドが並行して開発する

それぞれ説明します。

graphql-rubyのDSLでGraphQLの型やフィールドを書く

これはgraphql-rubyをふつうに使うときとほぼ同じになります。まだ裏の実装ができていないので resolver を書かない点が違いといえます。

Types::QueryType = GraphQL::ObjectType.define do
  name 'Query'

  field :user do
    type Types::UserType
    argument :email, !types.String

    resolve ->(obj, args, ctx) {
      # まだ裏の実装がないので書かない
    }
  end
end

Types::UserType = GraphQL::ObjectType.define do
  name 'User'

  field :email, !types.String
  connection :articles, Types::ArticleType.connection_type
end

Types::ArticleType = GraphQL::ObjectType.define do
  name 'Article'

  field :title, !types.String
  field :body, !types.String
end

このように開発に必要な型とフィールドだけを書いていきます。

追加した型やフィールドのダミーデータを書く

モックサーバとして動かすには、サーバになんらかのデータを返してもらう必要があります。Apolloのgraphql-toolsで作れるモックサーバは、フィールドの型に応じてある程度ランダムにデータを返してくれるようになっています。しかし、実際に返ってくるであろうものに近いデータを返したほうがフロントエンドの開発ではありがたいということもあるでしょう。また、ダミーデータを見ればフィールドの表現しているものの雰囲気がわかるという利点もあります。

Apolloのモックサーバが返す値を指定するために、次のようなオブジェクトを定義します。ここでは仮に mocks.js とします。

// mock_app/mocks.js

module.exports = {
  User: () => ({
    email: 'kymmt90@example.com',
  }),
  Article: () => ({
    title: 'The Article',
    body: 'This is the article.',
  }),
};

GraphQLの型に対して、型のフィールドとダミーデータを持つオブジェクトを書き、それを返す関数を持たせているだけです。これを書いておくだけで、connectionなどを使ってクエリがネストしているときも、graphql-toolsのモックサーバがいい感じにダミーデータを返してくれるようになります。

レビュー

上述した流れでスキーマとダミーデータだけ書けたら、チームでレビューするなりして合意をとります。

モックサーバを立ち上げる

Apolloを使ってモックサーバを立ち上げます。

Expressとapollo-serverを使って、次のようなサーバを書きます。ここでは mock_app/index.js とします。

// mock_app/index.js

const express = require('express');
const bodyParser = require('body-parser');
const { graphqlExpress } = require('apollo-server-express');
const { addMockFunctionsToSchema, makeExecutableSchema } = require('graphql-tools');

// モックサーバの作成
const typeDefs = require('./type_defs');
const schema = makeExecutableSchema({ typeDefs });
const mocks = require('./mocks')
addMockFunctionsToSchema({ schema, mocks });

// GraphQLエンドポイントを持つExpressサーバの立ち上げ
const app = express();
app.use('/graphql', bodyParser.json(), graphqlExpress({ schema }));
app.listen(3000, () => {
  console.log('GraphQL mock server is running!!1');
});

mock_app/package.json は次のような感じです。

{
  "name": "graphql-mock-server",
  "private": true,
  "version": "0.0.1",
  "description": "graphql-mock-server",
  "author": "kymmt90",
  "dependencies": {
    "apollo-server-express": "*",
    "graphql-tools": "*",
    "graphql": "*",
    "express": "*",
    "body-parser": "*"
  }
}

index.js に書いたように、GraphQLスキーマを type_defs.js から読み込みます。この type_defs.js を得るために、graphql-rubyで定義したスキーマをもとに、次のRakeタスクを書き、スキーマをダンプできるようにします。

# lib/tasks/graphql.rake
namespace :graphql do
  namespace :schema do
    desc 'Dump GraphQL schema as a JavaScript file'
    task dump_as_js: :environment do
      schema = AppSchema.to_definition
      File.open(Rails.root.join('mock_app', 'type_defs.js'), 'w') do |f|
        f.puts("module.exports = `\n")
        f.puts(schema)
        f.puts('`')
      end
    end
  end
end

bin/rails graphql:schema:dump_as_js を実行すると次のようなファイルが得られます。type_defs.js では、GraphQLスキーマをJSの文字列として定義しています。

// mock_app/type_defs.js

module.exports = `
type Article {
  body: String!
  title: String!
}

# The connection type for Article.
type ArticleConnection {
  # A list of edges.
  edges: [ArticleEdge]

  # Information to aid in pagination.
  pageInfo: PageInfo!
}

# An edge in a connection.
type ArticleEdge {
  # A cursor for use in pagination.
  cursor: String!

  # The item at the end of the edge.
  node: Article
}

# Properties for creating an article by a specified user
input ArticleInputType {
  # Body of the article
  body: String

  # Title of the article
  title: String!

  # Email address of the user
  user_email: String!
}

type Mutation {
  # Create an article by the specified user
  createArticle(article: ArticleInputType): Article
}

pp# Information about pagination in a connection.
type PageInfo {
  # When paginating forwards, the cursor to continue.
  endCursor: String

  # When paginating forwards, are there more items?
  hasNextPage: Boolean!

  # When paginating backwards, are there more items?
  hasPreviousPage: Boolean!

  # When paginating backwards, the cursor to continue.
  startCursor: String
}

type Query {
  user(email: String!): User
}

type User {
  articles(
    # Returns the elements in the list that come after the specified global ID.
    after: String

    # Returns the elements in the list that come before the specified global ID.
    before: String

    # Returns the first _n_ elements from the list.
    first: Int

    # Returns the last _n_ elements from the list.
    last: Int
  ): ArticleConnection
  email: String!
}
`

ここまで来れば、あとは index.js をサーバとして起動すれば終わりです。

$ (cd mock_app && npm install && node start index)
GraphQL mock server is running!!1

次のように、サーバがGraphQLのクエリを受け取りつつ、自分で書いたダミーデータがサーバから返ってくるようになります。これでスタブサーバが手に入ったので、フロントエンドとバックエンドを並行開発していくことができます。

f:id:kymmt90:20171224113344p:plain

まとめ

Railsでgraphql-rubyを使っている場合に、Apolloのツールセットを使ってGraphQLのモックサーバを作る方法について説明しました。Apolloが便利なので、わりと簡単にセットアップできました。

GraphQL APIの開発方法はまだ模索段階なので、2018年はこれを業務に取り入れてみて気になる点がないか確かめていきたいという気持ちです。

参考

Railsの内部やPluginによる拡張方法について学べる本 "Crafting Rails 4 Applications" を読んだ

読みました、というよりは夏の終わりぐらいから何度か読んでいました。

Crafting Rails 4 Applications: Expert Practices for Everyday Rails Development (The Facets of Ruby)

Crafting Rails 4 Applications: Expert Practices for Everyday Rails Development (The Facets of Ruby)

どのような本か

Rails中上級者向けとよく言われている本です。Railsの内部構造を知りたかったり、Rails Plugin/Rails Engineを開発したいという人が読むと勉強になる内容です。著者はRailsのコントリビュータでありDeviseなどの開発で有名なPlatformatecの、というよりは、現在はElixirやPhoenixの開発で有名なといったほうがよさそうなJosé Valim氏です。

Railsの内部構造、Rails Plugin/Rails Engineの開発について書かれていると説明しましたが、具体的な例を挙げると次のような内容が含まれています。

  • 内部構造
    • Railsのレンダリングの仕組みを追う
    • Active Modelを構成するモジュールの構造を追う
  • Rails Plugin
    • コントローラで使うrender:pdfオプションを追加する
    • ビューテンプレートをファイルではなくDBから読み込めるようにする
    • ERBやHamlのようなテンプレートハンドラを新しく追加する
  • Rails Engine
    • Server Sent Eventsでストリーム通信できるようなRails Engineを作る
    • MongoDBにActiveSupport::Notificationsで得られるイベントを保存するMoutable and Isolated Engineを作る

どう役立つか

上にも書いたとおり、Rails Plugin/Rails Engineを作りたい人にとっては、例となるコードを通じてどうやればいいのかがわかるので便利だと思います。

本の題名のとおり、Rails 4(具体的にはv4.0.0)のコードを対象としています。なので、Rails 5のコードと照らし合わせながら読もうとすると、もちろん実装はRails 4から変わっており、サンプルコードなどはそのまま動かないものもあります(それはそれで勉強になりますが)。そういうわけで、Railsの内部については現在のものが直接わかるということにはならないのですが、Railsが実装されるうえで生まれたアイデアを学ぶというスタンスで読むと、Railsを使うときの周辺知識や文脈が補強されるのでよいのではと思います。

個人的には、この本でのRails PluginやRailtiesについての説明を参考にしながらSchemaConformistというRails Pluginを書けたので便利でした。

参考資料

この本を読み解くのには次の記事に集められた資料が役立ちました。

techracho.bpsinc.jp




おおざっぱには上述したような本です。残りは読書メモを貼っておきます。

  • Rails Plugin
    • Rails用に特化したgemのこと
    • rails new pluginでスケルトンのプロジェクトを生成できる
    • Railsアプリケーションを動かしてテストするために、プロジェクト内にデフォルトでtest/dummyという最小限のRailsプロジェクトが一式できる
  • Railsのレンダリングスタック
  • Active Model
    • Active Model準拠のAPIを持つクラスを作ると、コントローラやビューでRails Wayに乗って扱えるようになるので便利
  • Action View
    • ビューはform_forlink_toなどのヘルパーメソッドを持つビューコンテキストオブジェクト内で実行される
    • テンプレートを探す (lookup) ための情報を持つlookup contextをコントローラとビューで共有している
    • ActinoView::Resolver#find_templateをオーバーライドすると、ファイルシステム以外からテンプレートを読み出せる
    • ActionController::Metal
      • HTTPのことを知らないAbstractControllerとHTTPのことを全部知っているActionController::Baseの間の軽量なコントローラ
      • Rackアプリケーションとして動作し、HTTPを扱える最小限の機能を持つ
    • テンプレートハンドラ
      • テンプレートハンドラは内部APIActionView::Template.register_template_handlerで登録
      • テンプレートハンドラは「レンダリング後の文字列を返すRubyコード」の文字列表現を返すようにすればよい
  • Rails::Railtie
    • Railsの初期化とデフォルト設定にフックできる
    • 利用例
      • アプリ初期化時にタスクを実行したい
      • プラグインで設定値を変えたい
      • プラグインでRakeタスクを入れたい
      • Rails consoleかrunner実行時にプラグインでコードを実行したい
      • プラグインの設定値を追加したい
  • Moutable and Isolated Engines
    • rails plugin new foo --mootableでスケルトンを生成するRails Engine
    • 名前空間が独立なので、本体のアプリケーションのコンポーネントをオーバーライドしない
  • Rack middlewares
    • 使いたいコントローラの中でuse FooControllerMiddlewareとできる
  • Rakeタスクで:environmentを指定している意味
    • Rakeタスク実行前にRailsの初期化処理するために、config/environment.rbを実行したい
      • 初期化を必要とするRakeタスクは多数存在(例:rake db:migrate
    • これを指定することでアプリケーションを初期化することができる
    • DBへのアクセスやアプリケーション内のクラスを利用するときは:environmentが必要
  • 1ファイルからなるRailsアプリケーション
    • config.ruRails::Applicationの子クラスを設定してinitialize!してrun Rails.applicationしたものをrackupする