追踪 graphql-devise 中的 mount_graphql_devise_for 的处理过程

這是什麼?

mount_graphql_devise_for的处理是一个黑盒子,无法深入了解graphql_devise,所以这是我在追踪详细实现时的笔记。

前提 tí)

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

追踪mount_graphql_devise_for的处理

使用graphql-devise框架,执行bundle exec rails generate graphql_devise:install命令后,在routes.rb文件中会定义以下代码。本次我们将追踪这段代码执行的功能。

mount_graphql_devise_for 'User', at: 'graphql_auth'

mount_graphql_devise_for是在ActionDispatch::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’,并且在: ‘graphql_auth’处,传递给mount_graphql_devise_for函数的参数如下所示。

resource

=> “User”

options

=> {:at=>”graphql_auth”}

我们来看一下ResourceLoader#initialize的处理方式,因为这些参数是通过GraphqlDevise::ResourceLoader.new传递的。

# 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中包含了以哈希形式存储的信息,如与每个操作对应的类名,例如登录(login)和注销(logout)。

=> {: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>

如果@resource是一个字符串,那么它会返回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’。

因此,模型最终将包含User(常量)。

如果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_type 返回的是 Types::UserType。

看起来下面的处理是在GraphqlDevise::Types::MutationType中添加了一个字段。

      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 中存在以下格式的哈希表,通过遍历哈希表中的每个元素,并将每个操作添加到 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中添加了字段。

      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中包含了类似下面的哈希表,对哈希表进行迭代,通过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

如果@routing不是非常重要的处理,则跳过GraphqlDevise.mount_resource(model)。

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#auth似乎对应着GraphqlDevise::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#authは GraphqlDevise::GraphqlController#authに対応

广告
将在 10 秒后关闭
bannerAds