LoginSignup
1

posted at

updated at

Organization

graphql-deviseのmount_graphql_devise_forの処理を追う

これは何か

mount_graphql_devise_for の処理がブラックボックスでgraphql_deviseの理解が進まないため、理解を進めるために詳細な実装を追ったときのメモです。

前提

  • graphql-devise v0.18.2
  • まだ、マイナーバージョンのgemで内部の構造は結構な頻度で変わっている模様で、これは2022/04/27時点の情報です。
  • 間違っている箇所などあるかもしれません。間違いありましたら、優しくご指摘いただけると嬉しいです。

mount_graphql_devise_forの処理を追う

graphql-deviseでbundle exec rails generate graphql_devise:installを実行してroutes.rbを見ると下記のコードが定義される。今回はこの処理が何をしているのかを追っていきたいと思います。

routes.rb
mount_graphql_devise_for 'User', at: 'graphql_auth'

mount_graphql_devise_forActionDispatch::Routing::Mapperに定義されているメソッドです。

# https://github.com/graphql-devise/graphql_devise/blob/v0.18.2/lib/graphql_devise/rails/routes.rb

module ActionDispatch::Routing
  class Mapper
    def mount_graphql_devise_for(resource, options = {})
      clean_options = GraphqlDevise::ResourceLoader.new(resource, options, true).call(
        GraphqlDevise::Types::QueryType,
        GraphqlDevise::Types::MutationType
      )

      post clean_options.at, to: 'graphql_devise/graphql#auth'
      get  clean_options.at, to: 'graphql_devise/graphql#auth'
    end
  end
end

mount_graphql_devise_for 'User', at: 'graphql_auth'が実行されると、mount_graphql_devise_forの引数には次のように値が渡されていました。

  • resource
    • => "User"
  • options
    • => {:at=>"graphql_auth"}

これらの引数はGraphqlDevise::ResourceLoader.newで引き渡されているのでResourceLoader#initializeの処理を見てみます。

# https://github.com/graphql-devise/graphql_devise/blob/v0.18.2/lib/graphql_devise/resource_loader.rb

module GraphqlDevise
  class ResourceLoader
    def initialize(resource, options = {}, routing = false)
      @resource           = resource
      @options            = options
      @routing            = routing
      @default_operations = GraphqlDevise::DefaultOperations::MUTATIONS.merge(GraphqlDevise::DefaultOperations::QUERIES)
    end
    
    

GraphqlDevise::ResourceLoader.new(resource, options, true)した後のインスタンス変数には下記の値が入っていました。

  • @resource
    • => "User"
  • @options
    • => {:at=>"graphql_auth"}
  • @routing
    • => true

@default_operationsには下記のようにloginlogoutのように各actionに対応するクラス名などの情報がhashで入っていました。

=> {:login=>{:klass=>GraphqlDevise::Mutations::Login, :authenticatable=>true},
 :logout=>{:klass=>GraphqlDevise::Mutations::Logout, :authenticatable=>true},
 :sign_up=>{:klass=>GraphqlDevise::Mutations::SignUp, :authenticatable=>true, :deprecation_reason=>"use register instead"},
 :register=>{:klass=>GraphqlDevise::Mutations::Register, :authenticatable=>true},
 :update_password=>
  {:klass=>GraphqlDevise::Mutations::UpdatePassword,
   :authenticatable=>true,
   :deprecation_reason=>"use update_password_with_token instead"},
 :update_password_with_token=>{:klass=>GraphqlDevise::Mutations::UpdatePasswordWithToken, :authenticatable=>true},
 :send_password_reset=>
  {:klass=>GraphqlDevise::Mutations::SendPasswordReset,
   :authenticatable=>false,
   :deprecation_reason=>"use send_password_reset_with_token instead"},
 :send_password_reset_with_token=>{:klass=>GraphqlDevise::Mutations::SendPasswordResetWithToken, :authenticatable=>false},
 :resend_confirmation=>
  {:klass=>GraphqlDevise::Mutations::ResendConfirmation,
   :authenticatable=>false,
   :deprecation_reason=>"use resend_confirmation_with_token instead"},
 :resend_confirmation_with_token=>{:klass=>GraphqlDevise::Mutations::ResendConfirmationWithToken, :authenticatable=>false},
 :confirm_registration_with_token=>{:klass=>GraphqlDevise::Mutations::ConfirmRegistrationWithToken, :authenticatable=>true},
 :confirm_account=>
  {:klass=>GraphqlDevise::Resolvers::ConfirmAccount,
   :deprecation_reason=>"use the new confirmation flow as it does not require this query anymore"},
 :check_password_token=>
  {:klass=>GraphqlDevise::Resolvers::CheckPasswordToken,
   :deprecation_reason=>"use the new password reset flow as it does not require this query anymore"}}

GraphqlDevise::ResourceLoaderのインスタンスが作られた後は、call(GraphqlDevise::Types::QueryType, GraphqlDevise::Types::MutationType)しているのでcallメソッドの処理を追ってみます。

# https://github.com/graphql-devise/graphql_devise/blob/v0.18.2/lib/graphql_devise/resource_loader.rb

    def call(query, mutation)
      # clean_options responds to all keys defined in GraphqlDevise::MountMethod::SUPPORTED_OPTIONS
      clean_options = GraphqlDevise::MountMethod::OptionSanitizer.new(@options).call!

      model = if @resource.is_a?(String)
        ActiveSupport::Deprecation.warn(<<-DEPRECATION.strip_heredoc, caller)
          Providing a String as the model you want to mount is deprecated and will be removed in a future version of
          this gem. Please use the actual model constant instead.
          EXAMPLE
          GraphqlDevise::ResourceLoader.new(User) # instead of GraphqlDevise::ResourceLoader.new('User')
          mount_graphql_devise_for User # instead of mount_graphql_devise_for 'User'
        DEPRECATION
        @resource.constantize
      else
        @resource
      end

      # Necesary when mounting a resource via route file as Devise forces the reloading of routes
      return clean_options if GraphqlDevise.resource_mounted?(model) && @routing

      validate_options!(clean_options)

      authenticatable_type = clean_options.authenticatable_type.presence ||
                             "Types::#{@resource}Type".safe_constantize ||
                             GraphqlDevise::Types::AuthenticatableType

      prepared_mutations = prepare_mutations(model, clean_options, authenticatable_type)

      if prepared_mutations.any? && mutation.blank?
        raise GraphqlDevise::Error, 'You need to provide a mutation type unless all mutations are skipped'
      end

      prepared_mutations.each do |action, prepared_mutation|
        mutation.field(action, mutation: prepared_mutation, authenticate: false)
      end

      prepared_resolvers = prepare_resolvers(model, clean_options, authenticatable_type)

      if prepared_resolvers.any? && query.blank?
        raise GraphqlDevise::Error, 'You need to provide a query type unless all queries are skipped'
      end

      prepared_resolvers.each do |action, resolver|
        query.field(action, resolver: resolver, authenticate: false)
      end

      GraphqlDevise.add_mapping(GraphqlDevise.to_mapping_name(@resource).to_sym, @resource)
      GraphqlDevise.mount_resource(model) if @routing

      clean_options
    end

    

GraphqlDevise::MountMethod::OptionSanitizer.new(@options).call!ではサニタイズ処理がごにょごにょされていました。ここはあまり重要な箇所ではなさそうなので深くは追わないことにしました。

clean_optionsには、下記のようにStructのインスタンスが入るみたいです。

GraphqlDevise::MountMethod::OptionSanitizer.new(@options).call!
=> #<struct
 at="graphql_auth",
 operations={},
 only=[],
 skip=[],
 additional_queries={},
 additional_mutations={},
 authenticatable_type=nil>

if @resource.is_a?(String)はtrueを返すので、trueのときの処理が実行されます。

ActiveSupport::Deprecation.warnは実行されると、下記のように警告文を出力するみたいです。

=> "DEPRECATION WARNING: Providing a String as the model you want to mount is deprecated and will be removed in a future version of\nthis gem. Please use the actual model constant instead.\nEXAMPLE\nGraphqlDevise::ResourceLoader.new(User) # instead of GraphqlDevise::ResourceLoader.new('User')\nmount_graphql_devise_for User # instead of mount_graphql_devise_for 'User'\n (called from block in <main> at /api/config/routes.rb:7)"

mount_graphql_devise_for 'User', at: 'graphql_auth'としている箇所の'User'は文字列ではなく、定数を使ってくださいというものでした。

ですので、modelには最終的にUser(定数)が入ります。

次のif GraphqlDevise.resource_mounted?(model) && @routingはfalseになります。

      # Necesary when mounting a resource via route file as Devise forces the reloading of routes
      return clean_options if GraphqlDevise.resource_mounted?(model) && @routing

validate_options!(clean_options)ではclean_optionsの検証を行っているみたいです。

authenticatable_typeTypes::UserTypeが返ってきました。

次の処理はGraphqlDevise::Types::MutationTypeにfieldを追加しているようでした。

      prepared_mutations = prepare_mutations(model, clean_options, authenticatable_type)

      if prepared_mutations.any? && mutation.blank?
        raise GraphqlDevise::Error, 'You need to provide a mutation type unless all mutations are skipped'
      end

      prepared_mutations.each do |action, prepared_mutation|
        mutation.field(action, mutation: prepared_mutation, authenticate: false)
      end

prepared_mutationsには次のようなhashが入ってきて、hashをeachして各actionをmutation.fieldで追加されていました。

=> {:user_login=>#<Class:0x0000561b0fdcdf40>,
 :user_logout=>#<Class:0x0000561b0fe82738>,
 :user_sign_up=>#<Class:0x0000561b0fef7100>,
 :user_register=>#<Class:0x0000561b0ffc2b98>,
 :user_update_password=>#<Class:0x0000561b100b8368>,
 :user_update_password_with_token=>#<Class:0x0000561b101a1310>,
 :user_send_password_reset=>#<Class:0x0000561b10276650>,
 :user_send_password_reset_with_token=>#<Class:0x0000561b102e4dd0>,
 :user_resend_confirmation=>#<Class:0x0000561b103515c0>,
 :user_resend_confirmation_with_token=>#<Class:0x0000561b1031f9a8>,
 :user_confirm_registration_with_token=>#<Class:0x0000561b103b27a8>}

念のため確かめてみました。追加されています。

GraphqlDevise::Types::MutationType.fields
=> {"userLogin"=>#<GraphQL::Schema::Field Mutation.userLogin(...): UserLoginPayload>,
 "userLogout"=>#<GraphQL::Schema::Field Mutation.userLogout: UserLogoutPayload>,
 "userSignUp"=>#<GraphQL::Schema::Field Mutation.userSignUp(...): UserSignUpPayload>,
 "userRegister"=>#<GraphQL::Schema::Field Mutation.userRegister(...): UserRegisterPayload>,
 "userUpdatePassword"=>#<GraphQL::Schema::Field Mutation.userUpdatePassword(...): UserUpdatePasswordPayload>,
 "userUpdatePasswordWithToken"=>
  #<GraphQL::Schema::Field Mutation.userUpdatePasswordWithToken(...): UserUpdatePasswordWithTokenPayload>,
 "userSendPasswordReset"=>#<GraphQL::Schema::Field Mutation.userSendPasswordReset(...): UserSendPasswordResetPayload>,
 "userSendPasswordResetWithToken"=>
  #<GraphQL::Schema::Field Mutation.userSendPasswordResetWithToken(...): UserSendPasswordResetWithTokenPayload>,
 "userResendConfirmation"=>#<GraphQL::Schema::Field Mutation.userResendConfirmation(...): UserResendConfirmationPayload>,
 "userResendConfirmationWithToken"=>
  #<GraphQL::Schema::Field Mutation.userResendConfirmationWithToken(...): UserResendConfirmationWithTokenPayload>,
 "userConfirmRegistrationWithToken"=>
  #<GraphQL::Schema::Field Mutation.userConfirmRegistrationWithToken(...): UserConfirmRegistrationWithTokenPayload>}

次の処理も同じような感じですが、今度はGraphqlDevise::Types::QueryTypeにfieldを追加しているようでした。

      prepared_resolvers = prepare_resolvers(model, clean_options, authenticatable_type)

      if prepared_resolvers.any? && query.blank?
        raise GraphqlDevise::Error, 'You need to provide a query type unless all queries are skipped'
      end

      prepared_resolvers.each do |action, resolver|
        query.field(action, resolver: resolver, authenticate: false)
      end

prepared_resolversには次のようなhashが入ってきて、hashをeachして各actionをquery.fieldで追加されていました。

=> {:user_confirm_account=>#<Class:0x0000561b107ea550>, :user_check_password_token=>#<Class:0x0000561b108945f0>}

念のため確かめてみました。追加されています。

GraphqlDevise::Types::QueryType.fields
=> {"userConfirmAccount"=>#<GraphQL::Schema::Field Query.userConfirmAccount(...): User!>,
 "userCheckPasswordToken"=>#<GraphQL::Schema::Field Query.userCheckPasswordToken(...): User!>}

GraphqlDevise.add_mapping(GraphqlDevise.to_mapping_name(@resource).to_sym, @resource)では内部的にDevise.add_mappingが実行されていました。

# https://github.com/graphql-devise/graphql_devise/blob/v0.18.2/lib/graphql_devise.rb

  def self.add_mapping(mapping_name, resource)
    return if Devise.mappings.key?(mapping_name.to_sym)

    Devise.add_mapping(
      mapping_name.to_s.pluralize.to_sym,
      module: :devise, class_name: resource.to_s
    )
  end

Devise.add_mappingは下記のようなメソッドでした。

  • Devise::Mapping.newで主にやっていること
    • mappingオブジェクトを返す(参考)
  • h.define_helpersで主にやっていること
    • authenticate_{mapping}!current_{mapping}、や{mapping}_signed_in?などのヘルパーメソッドをmappingに基づいて作成している(参考)
# https://github.com/heartcombo/devise/blob/main/lib/devise.rb#L353

  # Small method that adds a mapping to Devise.
  def self.add_mapping(resource, options)
    mapping = Devise::Mapping.new(resource, options)
    @@mappings[mapping.name] = mapping
    @@default_scope ||= mapping.name
    @@helpers.each { |h| h.define_helpers(mapping) }
    mapping
  end

GraphqlDevise.mount_resource(model) if @routingはあまり重要ではなさそうな処理なのでスルーします。

GraphqlDevise::ResourceLoader.new(resource, options, true).callは最終的にclean_optionsを返すので、mount_graphql_devise_for の中の下記のルーティングは

post clean_options.at, to: 'graphql_devise/graphql#auth'
get  clean_options.at, to: 'graphql_devise/graphql#auth'

から、最終的に下記になります。

post 'graphql_auth', to: 'graphql_devise/graphql#auth'
get  'graphql_auth', to: 'graphql_devise/graphql#auth'

そして、graphql_devise/graphql#authGraphqlDevise::GraphqlController#authに対応するようでした。

mount_graphql_devise_forの処理まとめ

  • 内部でGraphqlDevise::ResourceLoaderを呼び出しており、主に次のことをやっていました
    • GraphqlDevise::Types::MutationTypeに各actionのfieldを追加
    • GraphqlDevise::Types::QueryTypeに各actionのfieldを追加
    • Devise.add_mappingを呼び出してauthenticate_{mapping}!current_{mapping}、や{mapping}_signed_in?などのヘルパーメソッドを作成
  • ルーティングの定義をしていました
    • post 'graphql_auth', to: 'graphql_devise/graphql#auth'
    • get 'graphql_auth', to: 'graphql_devise/graphql#auth'
    • graphql_devise/graphql#authGraphqlDevise::GraphqlController#authに対応

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1