Skip to content

Instantly share code, notes, and snippets.

@masarakki
Last active July 4, 2023 17:44
Show Gist options
  • Save masarakki/d3e3f57bd1f2f38a0b8a18bbc54d8d89 to your computer and use it in GitHub Desktop.
Save masarakki/d3e3f57bd1f2f38a0b8a18bbc54d8d89 to your computer and use it in GitHub Desktop.
GraphQL association DSL for using GraphQL::Batch automatically

modules

# app/graphql/association/dsl.rb
module Association
  module Dsl
    extend ActiveSupport::Concern

    class_methods do
      def association(name, *args, policy: nil, source: nil, **options)
        field name, *args, **options
        source ||= name

        define_method name do
          policy_scope = policy&.then do
            scope = "#{policy}Policy::Scope".constantize
            policy_scope = scope.new(context[:current_user], policy).resolve
          end

          Association::Loader.for(object.class, source.to_sym).scoped(policy_scope).load(object)
        end
      end

      def attachment(name, type, *args, source: nil, **options)
        source ||= name
        source = type.is_a?(Array) ? "#{source}_blobs" : "#{source}_blob"

        association name, type, *args, source: source.to_sym, **options
      end
    end
  end
end

# app/graphql/association/loader.rb
module Association
  class Loader
    # 公式ドキュメントのassociation_loader (https://github.com/Shopify/graphql-batch/blob/main/examples/association_loader.rb) との差分
    
    def scoped(policy_scope = nil)
      @policy_scope = policy_scope
      self
    end

    def preload_association(records)
      if Rails::VERSION::MAJOR < 7
        ActiveRecord::Associations::Preloader.new.preload(records, @association_name, @policy_scope)
      else
        ActiveRecord::Associations::Preloader.new(records: records,
                                                  associations: @association_name,
                                                  scope: @policy_scope).call
      end
    end
  end
end

usage

# app/graphql/types/user_type.rb
module Types
  class UserType < Types::BaseType
    field :name, String, null: false
    association :posts, [Types::PostType], policy: Post, null: false
    attachment :icon, Types::IconType, null: false
  end
end

# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      scope.where(private: false).or(scope.where(user_id: current_user.id))
    end
  end
end

解説

何も考えずともすべての関連テーブルへのアクセスをGraphQL::Batch化しN+1問題を解決する。 association というメソッドで関連テーブルを宣言するだけで自動で上手いことやってくれる。 いちいちincludesを考えなければならない昔ながらのRailsよりも遥かに便利。 punditによる制限もちゃんと機能する。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment