クエリ結果を軸としたGraphQLのエラーハンドリング
この記事は GraphQL Advent Calendar 2020 6日目の記事です。
前回の記事は @fossamagna さんの AppSyncのGraphQL APIを@apollo/clientで呼び出す でした。
この記事では以下の記事で紹介されているGraphQLのエラーハンドリングの手法についての紹介と、それを利用するクライアントサイドのメリットについての考察をしていきます。 sachee.medium.com
アプリケーションで生じる様々なエラーと、GraphQLの一般的なハンドリング
GraphQLはリクエストに対してエラーが発生した場合、一般的にレスポンス中のerrorsというキーの中にそのエラーに関する情報を詰め込んだレスポンスを返すというプラクティスがあります。
"errors": [ { "message": "....", "locations": [ ... ], "path": [...], "extensions": { ... }, } ]
このフォーマットでは、認証エラーやinternal server errorというコンテキストの異なったエラーなどが並列な存在として表現されてしまうので、適切にエラーをハンドリングするためには少し工夫をする必要があります。
実際に世にある様々なGraphQLのライブラリ(graphql-ruby, apollo-server など)ではこのフォーマットを踏襲し、また、エラーを分類可能にさせるためにextensionsにどのようなエラーなのかという情報を入れ込むなどして表現するようにしています。
クエリ対象をリクエストの結果としてモデリングしてみる
errorsに埋め込むことでどこでエラーが出ているのか、何が原因なのか、というところを把握することには事足りそうですが、これを利用するクライアントの立場に立ってみると意外と使いにくいと感じる点がありそうです。 例えばUserを取得するクエリ
{ user(username: "hoge") { id name } }
に対してエラーが発生する場合、レスポンスとしては以下などが返ってくるかと思います。
{ ..., "errors": [ { "path": [ "user" ], "locations": [ ... ], "extensions": { "message": "認証エラー", "code": "UNAUTHENTICATED" } } ] }
この時Userのエラーが存在するかを確かめるためには、リクエストの度にerrors中にそれが存在するのかを配列を舐めて見ていく必要があり、また、エラーの取捨選択や、その詳細度をクエリでコントロールできないため、クライアント側で取り扱う際に不便そうです。
そこでこちらの記事で提案されている、リソースではなく、クエリの結果
をクエリ対象としてモデリングした場合を考えてみます。
sachee.medium.com
この クエリの結果
をクエリする考えを取り入れると、Userをクエリするのではなく、以下のようにリソースとエラーの内容をUnionとしてUserResultを定義することができます。
type User { id: ID! name: String } type IsBlocked { message: String blockedByUser: User } type UnAuthorized { reason: String role: String } union UserResult = User | IsBlocked | UnAuthorized
またこれをクエリする際は以下のようになります。
{ userResult(username: "hoge") { __typename ... on User { id name } ... on IsBlocked { message blockedByUser { username } } ... on UnAuthorized { reason role } }
このようにクエリできるようになったことによって、Userに関心があるものはUserResultでまとめて表現することができ、かつクライアント側でその詳細度をコントロールできるようになりました。
特にこの例では、UnAuthorizedな状態の時に、どのroleにおいて認証が通らなかったのかなどを表示する/しないはクライアント側の判断に委ねることができるので、管理画面などで表示したい場合はroleを含ませ、普段ユーザが使う場合は詳細度を絞った形で表現するなど、クライアント側がコントロールできるメリットが生じているのが感じられるかと思います。
また、副次的ではありますが、エラーのパターンを意識してサーバ側はスキーマの設計をする必要があることや、クライアント側はそれをクエリで意識的にクエリできるようになることから、どのようなエラーが発生しうるのか、それをどのようにハンドリングするのか、ということをより意識した設計をすることが可能になるかと思います。
まとめ
- GraphQLでクエリする対象を、リソースではなく、場合によっては
クエリの結果
としてモデリングすることできる - クエリの結果としてモデリングすることによって、よりクライアント側が使いやすいエラーの設計が可能になる
プロジェクトの種類や、開発の段階によっては採用の善し悪しが別れそうですが、うまく取り入れられればクライアント側でエラーの内容を柔軟に取り扱うことができ、適切な情報を適切なユーザ/コンテキストで表現することができるようになるのでお試しいただければと思います!
Reactのイベントハンドリングについて
はじめに
この記事はReact #2 Advent Calendar 2019 11日目の記事です。
先日JSConfに参加し、今まであまり意識せずなんとなく使ってきたReactのイベントハンドリングについて個人的に新しい発見があったので、記事にしてみます。
本記事ではReactのイベントハンドリングの仕組みについてざっくりと理解することを目的としますが、より詳しい説明や、この仕組みによって puppeteer などでテストする際にこれによってどのような技術的チャレンジが行われているかはBenjamin Gruenbaumさんの The Anatomy of a Clickという発表から知ることができるのでぜひご視聴ください。
JavaScriptのイベント伝搬について
まずJavaScriptがイベントを取り扱うのかについて見ていきます。
通常クリックイベントをあるelement以下のDOMに付与しようとすると、以下のように書くと思います。
el.addEventListener('click', listener)
このように書くことによって、 el
以下のDOMがクリックされた時、listener
にそのイベントの発火が渡される形になります。このクリックからlistenerへの発火は、実はより細かく見ると、Capture Phase
Target Phase
Bubble Phase
というフェーズにわかれていて、上で記述した addEventListener
はBubbling Phase
でこのイベントリスナーが評価されるようになっている状態です。
逆にCapture Phase
でイベントの処理を行おうとする場合、以下のようにuseCapture
オプションをtrueにします。
el.addEventListener('click', listener, true)
こちらはW3C UIEvents specificationから引用した上記フェーズの説明図となります。
まずCapture Phase
では、Windowから順にその子要素を辿るようにイベントのターゲットである要素まで順々にイベントを処理していきます。
次にTarget Phase
では今伝搬しているイベントをstopPropagation
などで止めるかどうかなどを判定します。
最後にBubbling Phase
では、イベントターゲットから親の要素を辿りWindowまでイベントを伝搬していく形になります。
基本的にはBubbling
React的イベントハンドリング
ざっくりとJavaScriptにおけるイベントの仕組みについて把握したので、Reactではどのようにこのフェーズを取り扱っているのかを見ます。
Reactでは、そのイベントシステムの一部として、SyntheticEvent
というものが存在しています。
例えば、ReactのonClick
というSyntheticEvent
のイベントハンドラはユーザが起こすクリックに対するイベントの処理をBubbling Phase
で行うようになっており、Capture Phase
で処理を行おうとする場合は、onClickCapture
など、~Capture
というイベントハンドラを利用することでこれが可能になっています。
また、SyntheticEvent
はブラウザ間のイベントの仕様の違いなどを吸収してくれています。例えば、以下のようなonMouseEnter
のイベントを各種ブラウザでサポートするための以下リンク先の実装を見てみると
mouseOver
とmouseOver
イベントの組み合わせによってそれを判定していることがわかり、開発者があまり気にしなくても、クロスブラウザ対応をケアしてくれていることがわかるかと思います。
このような抽象化は開発者としてはとてもありがたいのですが、気をつけたいのは、これはonMouseEnter
をそのままmouseOver
とmouseOver
イベントの組み合わせに置き換えているため、直接onMouseEnter
を発火させた場合、Reactはそのイベントを処理することができないことがある可能性があるということです。テストなどを行う際に、この仕組みを知っていると、もしかしたらデバッグなどに役立つかもしれません。
まとめ
- JavaScriptのイベントの処理の仕組みには
Capture Phase
Target Phase
Bubbling Phase
という3つのフェーズが存在している - Reactは基本的には
Bubbling Phase
でイベントの処理をするようになっているが、Capture Phase
で処理するイベントハンドラも存在している - Reactは
SyntheticEvent
を使ってイベントを抽象化しているため、単純にイベントを同じような名前のイベントを直接発火させたとしても、反応しない場合があるので気をつける必要がある
Herokuのreview appsでアプリケーション毎にelasticsearchを利用する
review appsでもelasticsearchを無料プラン内で自動的に設定して、運用をしてみたかったので色々試してみたらできた記録
1. searchkickを導入する
elasticsearchの機能/設定を簡単にrailsに組み込むために、こちらのgemを利用する
Gemfile
gem 'searchkick'
して、 bundle install
2. herokuのaddonとしてbosaiを追加する
staging環境に関して、いつもaddonを追加している通り、market placeなどからbosaiを検索し、これを追加する
3. ELASTICSEARCH_URLにBONSAI_URLを設定する
searchkickはELASTICSEARCH_URLをelastic searchで参照できる場所と認識するため、こちらにはBONSAI_URLを設定する必要がある
そのために、review appsにbonsaiが追加され、その環境変数が設定された後に以下のようなrake taskを lib/tasks/bootstrap_review_app_tasks.rake
などといったファイルとして追加する
ここで定義しているタスクは、review appsの環境変数 ELASTICSEARCH_URL
に対して、ENV['BONSAI_URL']
で取得できる値を設定するというものになっている
また、 HEROKU_API_TOKEN
に関してはherokuのプロフィールページから参照することができるので、そちらを利用する
desc 'Bootstrap review app' namespace :dev do task bootstrap: ['db:schema:load', 'db:seed_fu'] do heroku = PlatformAPI.connect_oauth(ENV['HEROKU_API_TOKEN']) heroku.config_var.update(ENV['HEROKU_APP_NAME'], 'ELASTICSEARCH_URL' => ENV['BONSAI_URL']) end end
4. review appsの起動スクリプトを編集する
3で定義したスクリプトを実行する処理をapp.jsonのpostdeployの項に対して以下のように記述する
postdeployなどに関してはこちらを参照:Release Phase | Heroku Dev Center
{ "scripts": { "postdeploy": "bundle exec rake dev:bootstrap" } }
Layout/EmptyLineAfterGuardClause
Rubocopの0.59.0からLayout/EmptyLineAfterGuardClauseというcopがデフォルトになったみたい
早期リターンの後に行を空けると、どこまでが早期リターンで、どこからが中身の処理なのかがわかりやすくなるから良いよね
STIを利用している場合のFactoryBotのfactory定義について
STIを活用しているモデルは、スーパークラスでfactoryを定義して、テストでオブジェクトの比較をすると、スーパークラスのままのものと、 サブクラスに変換されているものとの比較になってしまうことがあり、そのためテストが落ちることがある
以下のように書くことによって、typeを元にSTIのクラスを取得し、それを使ってオブジェクトが初期化できるようになる
FactoryBot.define do factory :factory_name do initialize_with do klass = type.constantize klass.new(attributes) end # 以下factoryの定義 end end
RSpecのexpectとis_expectedの挙動の違いについて
subject(:actual_object) { ... } it { is_expected.to eq(expected_object) }
と
subject(:actual_object) { ... }
it { expect(actual_object).to eq(expected_object) }
では挙動が異なるという話
上の方を actual_object
と expected_object
を同じ属性などを定義した状態で比較しようとすると、
Compared using equal?, which compares object identity, but expected and actual are not the same object. Use
expect(actual).to eq(expected)
if you don't care about object identity in this example
という注意書きが表示され、上の方法は同じオブジェクトかどうかをobjectのIDで判定していることがわかる
一方で下の方は属性の一致で比較をするため、別オブジェクトであっても、属性さえ一致していればテストが通ることになる
つまり、同じオブジェクトかどうかを比較したい場合は上の方法、同じ属性を持っているかどうかを比較したい場合は下の方の方法を使えば良さそうだ
Model周りのRSpecを書く時に意識していること
正しい方法かはわからないけど、最近Model周りのテストコードを書いていて自分でしっくりくるような書き方を考え出したのでつらつらと書いていきたい
テストコードの基本形
あるModel内のインスタンスメソッドをテストする場合の基本形としては、このような形になってくる
describe '#instance_method' do subject { instance.instance_method } context 'when first context' do let(:instance) { build(:factory_for_first_context) } it { is_expected.to ... } end context 'when second context' do let(:instance) { build(:factory_for_second_context) } it { is_expected.to ... } end end
意識している箇所
1. describeの直下の行にはsubjectを置く
describe '#instance_method' do subject { instance.instance_method }
このようにdescribeでテストしたいメソッドを記述し、その下に今回のテスト対象をsubjectと利用して定義する
2. contextでそのコンテキストに合致したinstanceを定義
context 'when first context' do let(:instance) { build(:factory_for_first_context) }
contextで条件を記述しつつ、それに対応したinstanceをなかで定義することにより、その下の
it { is_expected.to ... }
でsubjectが評価される時に、contextのスコープで定義したinstanceが採用されるようになる 別のコンテキストでテストしたい時も同じようにそのcontextのスコープで定義を行えば、同等の結果が得られるので、すっきりとした印象のテストができる