✂️

GraphQL-Rubyとapollo-upload-clientを使って画像をs3にアップロードする(ActiveStrage使用)

2021/04/16に公開2

案件や個人開発では最近もっぱらGraphQL Rubyを用いて開発を行っています。
GraphQLを用いた際のメリットについては有用な記事がありますので、詳細は省かせていただきます。

GraphQL-Rubyを使用した開発(Rails + frontにReactやVueを用いる場合)はとても快適ではあります。
一方、番号付きページネーションや、画像のアップロードなどはGem kaminariや、ActiveStrage(carrierwave, shrine等)の「これ使っとけばさくっといける」といったデファクトスタンダードのようなものは明確に普及しておらず、また、日本語記事も執筆時点ではそんなに多くない印象です。(ググリ力が低い可能性有)

ファイルアップロード機能は割と必要になるシチュエーションが多く、今回もGraphQL-Ruby + Reactを使用したサービスに必要になりました。
apollo-upload-clientを使用して、s3に画像をアップロードしてみます。

バージョンとか

  • apollo-upload-client 14.1.3
  • graphql 15.3.0
  • ruby 2.7.1
  • Rails 6.0.3.5
  • apollo/client 3.1.2

実装

apollo-upload-clientのセットアップ

まずは、apollo-upload-clientのセットアップ項目にあるように

npm install apollo-upload-client

を行います。
installが終わったら、apollo.ts ファイルにapollo-upload-clientを適用していきます。

apollo.ts
...
import { createUploadLink } from 'apollo-upload-client'
...
...
...

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: createUploadLink(),
});

link部分にcreateUploadLinkを記述します。画像アップロード機能実装前は、この部分はHttpLinkになっていると思います。

Apollo Client can only have 1 terminating Apollo Link that sends the GraphQL requests; if one such as HttpLink is already setup, remove it.

このように既存のリンクを置き換えることで、ファイルアップロード時にはcreateUploadLinkの機能が動作し、そうでない通常時には今まで通りのパラメータでPOSTが行われるようになります。

自分の場合はこの段階で型のエラーが起こってしまい、
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/47369
にあるような対策を行いました。

graphql_controllerをファイルを受け入れることのできる形に改造する

上記で少し触れたように、ファイルアップロードが行われた場合のパラメータと、それ以外の通常のパラメータの形は異なっています。
params.keysで抽出した際のキーは、通常時だと

["operationName", "variables", "query", "controller", "action", "graphql"]

となっていましたが、ファイルをアップロードした際のkeyは

 ["operations", "map", "1", "controller", "action"]

となっています。
通常の送信時に必要なデータは、ファイル送信の場合だとoperationsというキーの中に格納されているので、AppSchema.executeに渡すvariablesなどの値をそれぞれいい感じに整形して上げる必要があります。

また、ファイルアップロード時のkeyの他の部分も特徴的なところが有りました。
mapキーには、アップロードしたファイルの個数が入っています。一個アップロードした場合はこうなっていました。

"{\"1\":[\"variables.input.images.0\"]}"

"1"というキーには、ファイルの実体が入っています。

 #<ActionDispatch::Http::UploadedFile:0x00007f71501bd0f0
 @content_type="image/png",
 @headers="Content-Disposition: form-data; name=\"1\"; filename=\"cat_image.png\"\r\nContent-Type: image/png\r\n",
 @original_filename="sample_image.png",
 @tempfile=#<File:/tmp/RackMultipart20210416-9-1ey3bqs.png>>

この部分は、2つ画像をアップロードした場合はkeyに"1", "2"というようにどんどん追加されていくようです。そしてparamsの中の実際のimageを受け取りたい部分(operationsの中にargumentsがある)は

 "images"=>[nil],

というようにnilになっていました。

この時点で「これあってる?」という疑念が湧きましたが進めていきます。

operationsというキーがあれば、ファイルがPOSTされたと見ることができそうです。
ここで分岐を行います。

graphql_controller.rb
...

def execute
 if params[:operations].present?
   operations = prams.ensure_hash(params[:operations])
   operation_name = operations['operationName']
   files = ensure_hash(params[:map]).map { |k, _| params[k] }
   parameter = { 'input' => operations['variables']['input'].merge({ 'images' => files }) }
      variables = ActionController::Parameters.new(parameter)
      query = operations['query']
 else
 #ここは通常時の部分なので既存の項目
   variables = ensure_hash(params[:variables])
   query = params[:query]
   operation_name = params[:operationName]
 end
end

通常時のparamaterの形を見つつ、ファイルアップロード時も同じように渡せるように整形していきます。

 parameter = { 'input' => operations['variables']['input'].merge({ 'images' => files }) }

受け取る側にFileタイプを当てる

imagesとして引数を受け取るようにしたので、argumentsの型を指定します。

file.rb
class Types::Scalars::File < GraphQL::Schema::Scalar
  description 'file'

  def self.coerce_input(action_dispatch_uploaded_file, _context)
    action_dispatch_uploaded_file
  end
end

Fileという型を受け取ることのできるように自前で型を作成します。
定義したら、mutationで受け取る部分にFile型を記載しましょう。

argument :images, [Types::Scalars::File], required: false

ActiveStrageにファイルをアタッチできるようにする

https://railsguides.jp/active_storage_overview.html
基本的にはもうRailsの領域なので、ここに従ってセットアップをしていけば躓くポイントはないです。

s3へのアップロードの場合も、strage.ymlに必要な情報を埋め込みましょう。
resolverの部分も普通にRailsの記法で書くことができます。
こんな感じです。

user.images.attach(images)

(おまけ)画像URLを取得する

url_forを使用したいが、ヘルパーメソッドはそのままだとフロントでは呼べないので、modelとかにurlを取得できるようにメソッドを仕込んでおく。

user.rb

class User < ApplicationRecord
...
...
include Rails.application.routes.url_helpers #使えるようにする
...
..

def get_avatar_url
  url_for(avatar) if avatar.attached?
end

これをqueryで使えるようにしてあげればok

Discussion

kinjokinjo

非常に参考になりました!!

ファイルアップロードが行われた場合のパラメータと、それ以外の通常のパラメータの形は異なっています。

これはapollo-upload-clientが、ここで定められた仕様に沿っているためのようです。
https://github.com/jaydenseric/graphql-multipart-request-spec/tree/v2.0.0-alpha.2
varibalesの方をnullにし、mapの{数値: variables内の場所}形式で対応関係を示し、paramas[1や2など]にファイルを入れるという感じのようですね。

この形式をパースし、他のクエリと同じようにvariablesの中にファイルを入れてくれるRackミドルウェアがあるようです。これで他のクエリと同様にvariablesの方でファイルを取得することができました。これでoperations有無による分岐はしなくていいかもしれないですね。
https://github.com/jetruby/apollo_upload_server-ruby

この仕様やミドルウェアもApolloではない第三者が作っているっぽいので、変わったりしないか若干心配だったりはしてます。。

sabotensaboten

見ていただきありがとうございます!

参考URLもありがとうございます!

自分も別のプロジェクトでapollo_upload_server-ruby使わせていただいて、本記事のつらみポイントが解消されてだいぶスッキリしますよね!

生で分岐入れるとキーの関係でカオスなことになりそうなので今後もしqraphql-rubyでファイル周り触ることになった場合は仕様変更のリスクもありつつapollo_upload_server-rubyの方を勧めたいです笑