これは何か
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
を見ると下記のコードが定義される。今回はこの処理が何をしているのかを追っていきたいと思います。
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', 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
には下記のようにlogin
、logout
のように各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_type
はTypes::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#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
に対応