Discordの通知を完成させてからはこのブログとはまた別のStrapiを操作するエディタを作り直していました。

Strapiは便利な反面、デプロイが面倒というかデータを匿名で収集しますとかそういうのが気になり始めてもうRailsで作ったほうがいいかという結論に至りました。 今このブログを更新しているRailsは通常のRailsでWebpackerを利用していますが、rails new --apiで始めるつまるところのRails APIで別に作り始めました。 もともとStrapiの箇所はレコードもそこまで多く作っていなかったし、エディタもまだ作りかけの状態だったのでRails APIへの移行は割とすんなりいきました。 StrapiにはJWTの認証と画像のアップローダーなんかが予め用意されていましたが、当然これらは自分で用意していかなければなりません。 といってもRailsのデファクトスタンダード的なgemは当然今でもメンテナンスが続いているのでまだまだこの言語はかつてほどの人気はなくなったかもしれませんが、まだまだ有用だなと思う次第です。

私の考察はどうでもよいとして、RailsにはActive Storageがありますが個人的に出た当初に使ってこれは違うなと感じてからはShrineというライブラリを推しています。 Active Storageは今だとある程度変わってきたのかもしれませんが、それ以上にShrineはいろいろできるという記憶のもとに実装してみました。 概ねREADMEに書いてあるとおり進めていけば問題ないのですが、Rails APIで使っている以上はShrineが提供するView側のヘルパーが使えません。 そうなるとReactとShrineをどうやってつなげるかというのが今回の課題でした:

# config/initializers/shrine.rb
Shrine.plugin :upload_endpoint

まずはupload_endpointを有効にしました。

# routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      mount Shrine.upload_endpoint(:cache) => "/images"
    end
  end
end

続いてroutes.rbにマウントします。Active StorageだとRailsが提供するURLを使うはずでしたがShrineは自分で決めることができるのでポイント高い。

<input
  type="file"
  onChange={(e) => {
    const [file] = e.target.files;
    const formData = new FormData();

    formData.append('file', file);
  
    fetch(`/api/v1/images`, {
      method: 'POST',
      body: formData,
    })
  }}
/>

あとはこんな感じのコードを書けば200が返ってくるはずです。簡単ですね。 では何が今回辛かったのかというと、このShrineが返してくれたキャッシュ画像をサーバーに保存するにはどうすればよいのかという部分です。 喉元をすでに過ぎてしまったので解決策を書いちゃいますが、単に保存するname要素が違っただけでした。

# app/models/blog_post.rb
class BlogPost < ApplicationRecord
  include Shrine::Attachment(:image)
end

# app/controllers/api/v1/blog_post_controller.rb
class Api::V1::BlogPostsController < ApplicationController
  # Only allow a trusted parameter "white list" through.
  def blog_post_params
    params.require(:blog_post).permit(:image)
  end
end

サーバーに渡す際にparams[:image]ではなくparams[:image_data]として送信していたので、cache画像がそのまま保存されてしまったというわけ。 あくまでimage_dataというカラムはShrineが内部に使うもので、利用する側は直接ここにデータを入れないようにしましょうというのが今回の肝でした。

おまけ

https://stackoverflow.com/questions/17240106/what-is-the-best-way-to-convert-all-controller-params-from-camelcase-to-snake-ca

# File: config/initializers/json_param_key_transform.rb
# Transform JSON request param keys from JSON-conventional camelCase to
# Rails-conventional snake_case:
ActionDispatch::Request.parameter_parsers[:json] = lambda { |raw_post|
  # Modified from action_dispatch/http/parameters.rb
  data = ActiveSupport::JSON.decode(raw_post)

  # Transform camelCase param keys to snake_case
  if data.is_a?(Array)
    data.map { |item| item.deep_transform_keys!(&:underscore) }
  else
    data.deep_transform_keys!(&:underscore)
  end

  # Return data
  data.is_a?(Hash) ? data : { '_json': data }
}

RailsはRubyで書かれているのでJavaScriptのようなcamelCaseとの相性はあまりよくありません。 クライアント側でサーバーにパラメータを併せるよりも、Rails側がcamelCaseで対応すべきだということに気づきましたが、このStackOverflowの回答は素晴らしいですね。 ExpressでもSQL用にcamelとsnakeの切り分けが必要だったりしますが、Railsの場合はこうやってパッチみたいにファイルをおいておけば自動で変換してくれるので便利です。ただ今回はimage_dataimageDataに切り替える必要はなかったのでおまけとして残します。