追踪 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に対応