😎

Ruby on Rails での GraphQL API 実装時のTips |Offers Tech Blog

2022/12/22に公開

はじめに

こんにちは!!

プロダクト開発人材の副業転職プラットフォーム Offers を運営する株式会社 overflow 普通のバックエンドエンジニアの takkun7171 でございます。
最近の近況ですが、12 月の amazon のブラックフライデーで特に買うものがなくて、カニを買いました。🦀
自宅でひっそりとカニ鍋忘年会したいと思います。

Rails の GraphQL API について

最近 GraphQL API を Rails で実装したんですけど、
基本的にはググったら情報がたくさんあったのでサクサクと実装できました。
ですが所々調べてもよくわからず、つまずいた箇所があり、
調べた過程で得た知見を Tips としてまとめたいと思います。
誰かのかゆいところに手が届けばいいなと思ってます。

なお GraphQL の gem を bundle install している前提です。

resolvers は作ったほうがいい

query_type.rb に最初は直接書いていたんですけど、
それだと確実に肥大化し、見通しが悪くなるので
やはり app/graphql/resolvers ディレクトリを作って、
ファイルを分けたほうが筋が良さそうです。

その際、以下のように
GraphiQL での呼び出し例をコメントに書いておくと
動作確認が捗ると思います。

app/graphql/resolvers/member.rb
module Resolvers
  class Member < Resolvers::BaseResolver
    type Types::MemberType, null: true
    description "Find member by code"
    argument :code, String, required: true

# GraphiQL での呼び出し例
=begin
{
  member(code: "S6_t8_76Ua") {
    id
    name
    company {
      id
      name
    }
  }
}
=end
    def resolve(code:)
      Member.find_by(code: code)
    end
  end
end

mutationの引数について

単純な mutation はググればわかるのですが、
例えば course テーブルと member テーブルが多対多の関係にあり、
course_members テーブルを作成したとします。
この時に mutation で course_members テーブルに複数データを
配列で指定して、一気にデータ作成したい時、どう書けばいいのか
よくわかりませんでした。

結論としては以下のように、app/graphql/input_types ディレクトリを作成し、
mutation の引数の型ファイルを作ったらうまく行きました。

app/graphql/input_types/course_member.rb
# frozen_string_literal: true

module InputTypes
  class CourseMember < Types::BaseInputObject
    # ObjectTypesと名前がバッティングしないようにする
    graphql_name 'CourseMemberAttributes'
    argument :course_id, Integer, required: true
    argument :member_id, Integer, required: true
  end
end
app/graphql/mutations/create_course_member.rb
module Mutations
  class CreateCourseMember < BaseMutation
    field :created_row_count, Integer, null: true

    argument :list, [InputTypes::CourseMember], required: true

# GraphiQL での呼び出し例
=begin
mutation {
  CreateCourseMember(
    input:{
      list: [
        {
          courseId: 5
          memberId: 301
        },
        {
          courseId: 5
          memberId: 304
        }
      ]
    }
  ){
    createdRowCount
  }
}
=end
    def resolve(**args)
      list = args[:list]

      course_members = []

      # 指定したcourse_idやmember_idのデータが存在するかのチェックなどは割愛

      list.each do |row|
        course_member = CourseMember.create!(
          course_id: row.course_id,
          member_id: row.member_id
        )
        course_members << course_member
      end

      return {
        created_row_count: course_members.length
      }
    end
  end
end

authorized?とvisible?による認証・認可

GraphQL でデータを取得する際の認可は、
authorized?と visible?を使えば簡単に実装できます。

例えば company に紐付いている product というデータがあり、
ログインしてるユーザには自社の product しか見せたくない場合は
以下のように書きます。
もし他社の product を閲覧しようとした場合はデータが nil になります。

app/graphql/types/product.rb
# frozen_string_literal: true

module Types
  class ProductType < Types::BaseObject
    def self.authorized?(object, context)
      super
      return false if context[:current_user].blank?

      return false if object.company_id != context[:current_user].company_id

      return true
    end

    field :id, ID, null: false
    field :company_id, Integer, null: false
    field :name, String, null: false
  end
end

secret_config というデータがあり
管理者にしか見せたくない場合は以下の通りです。
管理者以外にはこのデータの存在そのものが隠されます。

app/graphql/types/secret_config.rb
# frozen_string_literal: true

module Types
  class SecretConfigType < Types::BaseObject
    def self.visible?(context)
      super
      # 管理者でしか見れないという条件
      return context[:current_user]&.admin?
    end
  
    field :id, ID, null: false
    field :val, String, null: false
  end
end

上記は object 単位での認可でしたが、
field に対しても visible?、authorized?の設定が可能で
このカラムは特定のロールの人にしか見せたくない、、というユースケースにも対応できます。
こちらの記事が詳しいので、読めばわかると思います
https://qiita.com/kyntk/items/e2ec0e191cd928e32663#field

他にも mutation や enum にも
visible?、authorized?が適用できるらしいです。

まとめ

今回は Rails での GraphQL API 実装時の Tips でした。
ここに書いたもの以外の項目、例えば N+1 問題の対処などは
ググれば沢山情報が出てくるので頑張ってください w
GraphQL API の実装は初めてで、他の言語ではどうなのかわからないんですけど
Rails での実装は便利な gem のおかげで、ラクな印象でした。

関連記事

https://zenn.dev/offers/articles/20220425-universal-attitude
https://zenn.dev/offers/articles/20220523-component-design-best-practice
https://zenn.dev/offers/articles/20220415-leader-and-manager-roles-in-overflow

Offers Tech Blog

Discussion