graphql_devise 0.11.4 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/CHANGELOG.md +8 -0
- data/README.md +187 -20
- data/app/controllers/graphql_devise/application_controller.rb +4 -5
- data/app/controllers/graphql_devise/concerns/set_user_by_token.rb +23 -0
- data/app/controllers/graphql_devise/graphql_controller.rb +2 -0
- data/app/helpers/graphql_devise/mailer_helper.rb +2 -2
- data/config/routes.rb +2 -0
- data/graphql_devise.gemspec +3 -3
- data/lib/generators/graphql_devise/install_generator.rb +28 -5
- data/lib/graphql_devise.rb +16 -4
- data/lib/graphql_devise/mount_method/operation_preparer.rb +2 -2
- data/lib/graphql_devise/mount_method/operation_preparers/resource_name_setter.rb +1 -1
- data/lib/graphql_devise/mutations/sign_up.rb +1 -5
- data/lib/graphql_devise/rails/routes.rb +5 -72
- data/lib/graphql_devise/resource_loader.rb +87 -0
- data/lib/graphql_devise/schema_plugin.rb +87 -0
- data/lib/graphql_devise/version.rb +1 -1
- data/spec/dummy/app/controllers/api/v1/graphql_controller.rb +6 -2
- data/spec/dummy/app/graphql/dummy_schema.rb +9 -0
- data/spec/dummy/app/graphql/interpreter_schema.rb +9 -0
- data/spec/dummy/app/graphql/types/mutation_type.rb +1 -1
- data/spec/dummy/app/graphql/types/query_type.rb +10 -0
- data/spec/dummy/config/routes.rb +1 -0
- data/spec/generators/graphql_devise/install_generator_spec.rb +21 -0
- data/spec/rails_helper.rb +0 -1
- data/spec/requests/graphql_controller_spec.rb +80 -0
- data/spec/requests/user_controller_spec.rb +180 -24
- data/spec/services/mount_method/operation_preparer_spec.rb +2 -2
- data/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb +1 -1
- data/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb +1 -1
- data/spec/services/mount_method/operation_preparers/resource_name_setter_spec.rb +1 -1
- data/spec/services/resource_loader_spec.rb +82 -0
- data/spec/services/schema_plugin_spec.rb +26 -0
- metadata +31 -5
- data/spec/support/generators/file_helpers.rb +0 -12
data/lib/graphql_devise.rb
CHANGED
@@ -18,12 +18,21 @@ module GraphqlDevise
|
|
18
18
|
@schema_loaded = true
|
19
19
|
end
|
20
20
|
|
21
|
-
def self.
|
22
|
-
@mounted_resources
|
21
|
+
def self.resource_mounted?(mapping_name)
|
22
|
+
@mounted_resources.include?(mapping_name)
|
23
23
|
end
|
24
24
|
|
25
|
-
def self.
|
26
|
-
@mounted_resources
|
25
|
+
def self.mount_resource(mapping_name)
|
26
|
+
@mounted_resources << mapping_name
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.add_mapping(mapping_name, resource)
|
30
|
+
return if Devise.mappings.key?(mapping_name)
|
31
|
+
|
32
|
+
Devise.add_mapping(
|
33
|
+
mapping_name.to_s.pluralize.to_sym,
|
34
|
+
module: :devise, class_name: resource
|
35
|
+
)
|
27
36
|
end
|
28
37
|
end
|
29
38
|
|
@@ -47,3 +56,6 @@ require 'graphql_devise/mount_method/option_sanitizer'
|
|
47
56
|
require 'graphql_devise/mount_method/options_validator'
|
48
57
|
require 'graphql_devise/mount_method/operation_preparer'
|
49
58
|
require 'graphql_devise/mount_method/operation_sanitizer'
|
59
|
+
|
60
|
+
require 'graphql_devise/resource_loader'
|
61
|
+
require 'graphql_devise/schema_plugin'
|
@@ -8,10 +8,10 @@ require_relative 'operation_preparers/custom_operation_preparer'
|
|
8
8
|
module GraphqlDevise
|
9
9
|
module MountMethod
|
10
10
|
class OperationPreparer
|
11
|
-
def initialize(
|
11
|
+
def initialize(mapping_name:, selected_operations:, preparer:, custom:, additional_operations:)
|
12
12
|
@selected_operations = selected_operations
|
13
13
|
@preparer = preparer
|
14
|
-
@mapping_name =
|
14
|
+
@mapping_name = mapping_name
|
15
15
|
@custom = custom
|
16
16
|
@additional_operations = additional_operations
|
17
17
|
end
|
@@ -35,7 +35,7 @@ module GraphqlDevise
|
|
35
35
|
|
36
36
|
{ authenticatable: resource }
|
37
37
|
else
|
38
|
-
clean_up_passwords
|
38
|
+
resource.try(:clean_up_passwords)
|
39
39
|
raise_user_error_list(
|
40
40
|
I18n.t('graphql_devise.registration_failed'),
|
41
41
|
errors: resource.errors.full_messages
|
@@ -48,10 +48,6 @@ module GraphqlDevise
|
|
48
48
|
def build_resource(attrs)
|
49
49
|
resource_class.new(attrs)
|
50
50
|
end
|
51
|
-
|
52
|
-
def clean_up_passwords(resource)
|
53
|
-
controller.send(:clean_up_passwords, resource)
|
54
|
-
end
|
55
51
|
end
|
56
52
|
end
|
57
53
|
end
|
@@ -1,80 +1,13 @@
|
|
1
1
|
module ActionDispatch::Routing
|
2
2
|
class Mapper
|
3
|
-
DEVISE_OPERATIONS = [
|
4
|
-
:sessions,
|
5
|
-
:registrations,
|
6
|
-
:passwords,
|
7
|
-
:confirmations,
|
8
|
-
:omniauth_callbacks,
|
9
|
-
:unlocks,
|
10
|
-
:invitations
|
11
|
-
].freeze
|
12
|
-
|
13
3
|
def mount_graphql_devise_for(resource, options = {})
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
clean_options = GraphqlDevise::MountMethod::OptionSanitizer.new(options).call!
|
18
|
-
|
19
|
-
GraphqlDevise::MountMethod::OptionsValidator.new(
|
20
|
-
[
|
21
|
-
GraphqlDevise::MountMethod::OptionValidators::SkipOnlyValidator.new(options: clean_options),
|
22
|
-
GraphqlDevise::MountMethod::OptionValidators::ProvidedOperationsValidator.new(
|
23
|
-
options: clean_options, supported_operations: default_operations
|
24
|
-
)
|
25
|
-
]
|
26
|
-
).validate!
|
27
|
-
|
28
|
-
devise_for(
|
29
|
-
resource.pluralize.underscore.tr('/', '_').to_sym,
|
30
|
-
module: :devise,
|
31
|
-
class_name: resource,
|
32
|
-
skip: DEVISE_OPERATIONS
|
4
|
+
clean_options = GraphqlDevise::ResourceLoader.new(resource, options, true).call(
|
5
|
+
GraphqlDevise::Types::QueryType,
|
6
|
+
GraphqlDevise::Types::MutationType
|
33
7
|
)
|
34
8
|
|
35
|
-
|
36
|
-
|
37
|
-
get clean_options.at, to: 'graphql_devise/graphql#auth'
|
38
|
-
end
|
39
|
-
|
40
|
-
# Avoid routes reload done by Devise
|
41
|
-
return if GraphqlDevise.resource_mounted?(resource)
|
42
|
-
|
43
|
-
authenticatable_type = clean_options.authenticatable_type.presence ||
|
44
|
-
"Types::#{resource}Type".safe_constantize ||
|
45
|
-
GraphqlDevise::Types::AuthenticatableType
|
46
|
-
|
47
|
-
prepared_mutations = GraphqlDevise::MountMethod::OperationPreparer.new(
|
48
|
-
resource: resource,
|
49
|
-
custom: clean_options.operations,
|
50
|
-
additional_operations: clean_options.additional_mutations,
|
51
|
-
preparer: GraphqlDevise::MountMethod::OperationPreparers::MutationFieldSetter.new(authenticatable_type),
|
52
|
-
selected_operations: GraphqlDevise::MountMethod::OperationSanitizer.call(
|
53
|
-
default: GraphqlDevise::DefaultOperations::MUTATIONS, only: clean_options.only, skipped: clean_options.skip
|
54
|
-
)
|
55
|
-
).call
|
56
|
-
|
57
|
-
prepared_mutations.each do |action, mutation|
|
58
|
-
GraphqlDevise::Types::MutationType.field(action, mutation: mutation)
|
59
|
-
end
|
60
|
-
|
61
|
-
prepared_queries = GraphqlDevise::MountMethod::OperationPreparer.new(
|
62
|
-
resource: resource,
|
63
|
-
custom: clean_options.operations,
|
64
|
-
additional_operations: clean_options.additional_queries,
|
65
|
-
preparer: GraphqlDevise::MountMethod::OperationPreparers::ResolverTypeSetter.new(authenticatable_type),
|
66
|
-
selected_operations: GraphqlDevise::MountMethod::OperationSanitizer.call(
|
67
|
-
default: GraphqlDevise::DefaultOperations::QUERIES, only: clean_options.only, skipped: clean_options.skip
|
68
|
-
)
|
69
|
-
).call
|
70
|
-
|
71
|
-
prepared_queries.each do |action, resolver|
|
72
|
-
GraphqlDevise::Types::QueryType.field(action, resolver: resolver)
|
73
|
-
end
|
74
|
-
|
75
|
-
Devise.mailer.helper(GraphqlDevise::MailerHelper)
|
76
|
-
|
77
|
-
GraphqlDevise.mount_resource(resource)
|
9
|
+
post clean_options.at, to: 'graphql_devise/graphql#auth'
|
10
|
+
get clean_options.at, to: 'graphql_devise/graphql#auth'
|
78
11
|
end
|
79
12
|
end
|
80
13
|
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module GraphqlDevise
|
2
|
+
class ResourceLoader
|
3
|
+
def initialize(resource, options = {}, routing = false)
|
4
|
+
@resource = resource
|
5
|
+
@options = options
|
6
|
+
@routing = routing
|
7
|
+
@default_operations = GraphqlDevise::DefaultOperations::MUTATIONS.merge(GraphqlDevise::DefaultOperations::QUERIES)
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(query, mutation)
|
11
|
+
mapping_name = @resource.to_s.underscore.tr('/', '_').to_sym
|
12
|
+
|
13
|
+
# clean_options responds to all keys defined in GraphqlDevise::MountMethod::SUPPORTED_OPTIONS
|
14
|
+
clean_options = GraphqlDevise::MountMethod::OptionSanitizer.new(@options).call!
|
15
|
+
|
16
|
+
return clean_options if GraphqlDevise.resource_mounted?(mapping_name) && @routing
|
17
|
+
|
18
|
+
validate_options!(clean_options)
|
19
|
+
|
20
|
+
authenticatable_type = clean_options.authenticatable_type.presence ||
|
21
|
+
"Types::#{@resource}Type".safe_constantize ||
|
22
|
+
GraphqlDevise::Types::AuthenticatableType
|
23
|
+
|
24
|
+
prepared_mutations = prepare_mutations(mapping_name, clean_options, authenticatable_type)
|
25
|
+
|
26
|
+
if prepared_mutations.any? && mutation.blank?
|
27
|
+
raise GraphqlDevise::Error, 'You need to provide a mutation type unless all mutations are skipped'
|
28
|
+
end
|
29
|
+
|
30
|
+
prepared_mutations.each do |action, prepared_mutation|
|
31
|
+
mutation.field(action, mutation: prepared_mutation, authenticate: false)
|
32
|
+
end
|
33
|
+
|
34
|
+
prepared_resolvers = prepare_resolvers(mapping_name, clean_options, authenticatable_type)
|
35
|
+
|
36
|
+
if prepared_resolvers.any? && query.blank?
|
37
|
+
raise GraphqlDevise::Error, 'You need to provide a query type unless all queries are skipped'
|
38
|
+
end
|
39
|
+
|
40
|
+
prepared_resolvers.each do |action, resolver|
|
41
|
+
query.field(action, resolver: resolver, authenticate: false)
|
42
|
+
end
|
43
|
+
|
44
|
+
GraphqlDevise.add_mapping(mapping_name, @resource)
|
45
|
+
GraphqlDevise.mount_resource(mapping_name) if @routing
|
46
|
+
|
47
|
+
clean_options
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def prepare_resolvers(mapping_name, clean_options, authenticatable_type)
|
53
|
+
GraphqlDevise::MountMethod::OperationPreparer.new(
|
54
|
+
mapping_name: mapping_name,
|
55
|
+
custom: clean_options.operations,
|
56
|
+
additional_operations: clean_options.additional_queries,
|
57
|
+
preparer: GraphqlDevise::MountMethod::OperationPreparers::ResolverTypeSetter.new(authenticatable_type),
|
58
|
+
selected_operations: GraphqlDevise::MountMethod::OperationSanitizer.call(
|
59
|
+
default: GraphqlDevise::DefaultOperations::QUERIES, only: clean_options.only, skipped: clean_options.skip
|
60
|
+
)
|
61
|
+
).call
|
62
|
+
end
|
63
|
+
|
64
|
+
def prepare_mutations(mapping_name, clean_options, authenticatable_type)
|
65
|
+
GraphqlDevise::MountMethod::OperationPreparer.new(
|
66
|
+
mapping_name: mapping_name,
|
67
|
+
custom: clean_options.operations,
|
68
|
+
additional_operations: clean_options.additional_mutations,
|
69
|
+
preparer: GraphqlDevise::MountMethod::OperationPreparers::MutationFieldSetter.new(authenticatable_type),
|
70
|
+
selected_operations: GraphqlDevise::MountMethod::OperationSanitizer.call(
|
71
|
+
default: GraphqlDevise::DefaultOperations::MUTATIONS, only: clean_options.only, skipped: clean_options.skip
|
72
|
+
)
|
73
|
+
).call
|
74
|
+
end
|
75
|
+
|
76
|
+
def validate_options!(clean_options)
|
77
|
+
GraphqlDevise::MountMethod::OptionsValidator.new(
|
78
|
+
[
|
79
|
+
GraphqlDevise::MountMethod::OptionValidators::SkipOnlyValidator.new(options: clean_options),
|
80
|
+
GraphqlDevise::MountMethod::OptionValidators::ProvidedOperationsValidator.new(
|
81
|
+
options: clean_options, supported_operations: @default_operations
|
82
|
+
)
|
83
|
+
]
|
84
|
+
).validate!
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module GraphqlDevise
|
2
|
+
class SchemaPlugin
|
3
|
+
DEFAULT_NOT_AUTHENTICATED = ->(field) { raise GraphqlDevise::UserError, "#{field} field requires authentication" }
|
4
|
+
|
5
|
+
def initialize(query: nil, mutation: nil, authenticate_default: true, resource_loaders: [], unauthenticated_proc: DEFAULT_NOT_AUTHENTICATED)
|
6
|
+
@query = query
|
7
|
+
@mutation = mutation
|
8
|
+
@resource_loaders = resource_loaders
|
9
|
+
@authenticate_default = authenticate_default
|
10
|
+
@unauthenticated_proc = unauthenticated_proc
|
11
|
+
|
12
|
+
# Must happen on initialize so operations are loaded before the types are added to the schema on GQL < 1.10
|
13
|
+
load_fields
|
14
|
+
end
|
15
|
+
|
16
|
+
def use(schema_definition)
|
17
|
+
schema_definition.tracer(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
def trace(event, trace_data)
|
21
|
+
# Authenticate only root level queries
|
22
|
+
return yield unless event == 'execute_field' && path(trace_data).count == 1
|
23
|
+
|
24
|
+
field = traced_field(trace_data)
|
25
|
+
provided_value = authenticate_option(field, trace_data)
|
26
|
+
|
27
|
+
if !provided_value.nil?
|
28
|
+
raise_on_missing_resource(context(trace_data), field) if provided_value
|
29
|
+
elsif @authenticate_default
|
30
|
+
raise_on_missing_resource(context(trace_data), field)
|
31
|
+
end
|
32
|
+
|
33
|
+
yield
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def raise_on_missing_resource(context, field)
|
39
|
+
@unauthenticated_proc.call(field.name) if context[:current_resource].blank?
|
40
|
+
end
|
41
|
+
|
42
|
+
def context(trace_data)
|
43
|
+
query = if trace_data[:context]
|
44
|
+
trace_data[:context].query
|
45
|
+
else
|
46
|
+
trace_data[:query]
|
47
|
+
end
|
48
|
+
|
49
|
+
query.context
|
50
|
+
end
|
51
|
+
|
52
|
+
def path(trace_data)
|
53
|
+
if trace_data[:context]
|
54
|
+
trace_data[:context].path
|
55
|
+
else
|
56
|
+
trace_data[:path]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def traced_field(trace_data)
|
61
|
+
if trace_data[:context]
|
62
|
+
trace_data[:context].field
|
63
|
+
else
|
64
|
+
trace_data[:field]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def authenticate_option(field, trace_data)
|
69
|
+
if trace_data[:context]
|
70
|
+
field.metadata[:authenticate]
|
71
|
+
else
|
72
|
+
field.graphql_definition.metadata[:authenticate]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def load_fields
|
77
|
+
@resource_loaders.each do |resource_loader|
|
78
|
+
raise Error, 'Invalid resource loader instance' unless resource_loader.instance_of?(GraphqlDevise::ResourceLoader)
|
79
|
+
|
80
|
+
resource_loader.call(@query, @mutation)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
GraphQL::Field.accepts_definitions(authenticate: GraphQL::Define.assign_metadata_key(:authenticate))
|
87
|
+
GraphQL::Schema::Field.accepts_definition(:authenticate)
|
@@ -3,10 +3,14 @@ module Api
|
|
3
3
|
class GraphqlController < ApplicationController
|
4
4
|
include GraphqlDevise::Concerns::SetUserByToken
|
5
5
|
|
6
|
-
before_action :
|
6
|
+
before_action -> { set_resource_by_token(:user) }
|
7
7
|
|
8
8
|
def graphql
|
9
|
-
render json: DummySchema.execute(params[:query])
|
9
|
+
render json: DummySchema.execute(params[:query], context: graphql_context)
|
10
|
+
end
|
11
|
+
|
12
|
+
def interpreter
|
13
|
+
render json: InterpreterSchema.execute(params[:query], context: graphql_context)
|
10
14
|
end
|
11
15
|
|
12
16
|
private
|
@@ -1,4 +1,13 @@
|
|
1
1
|
class DummySchema < GraphQL::Schema
|
2
|
+
use GraphqlDevise::SchemaPlugin.new(
|
3
|
+
query: Types::QueryType,
|
4
|
+
mutation: Types::MutationType,
|
5
|
+
resource_loaders: [
|
6
|
+
GraphqlDevise::ResourceLoader.new('User', only: [:login, :confirm_account]),
|
7
|
+
GraphqlDevise::ResourceLoader.new('Guest', only: [:logout])
|
8
|
+
]
|
9
|
+
)
|
10
|
+
|
2
11
|
mutation(Types::MutationType)
|
3
12
|
query(Types::QueryType)
|
4
13
|
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class InterpreterSchema < GraphQL::Schema
|
2
|
+
use GraphQL::Execution::Interpreter if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('1.9.0')
|
3
|
+
use GraphQL::Analysis::AST if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('1.10.0')
|
4
|
+
|
5
|
+
use GraphqlDevise::SchemaPlugin.new(query: Types::QueryType, authenticate_default: false)
|
6
|
+
|
7
|
+
mutation(Types::MutationType)
|
8
|
+
query(Types::QueryType)
|
9
|
+
end
|
@@ -1,5 +1,15 @@
|
|
1
1
|
module Types
|
2
2
|
class QueryType < Types::BaseObject
|
3
3
|
field :user, resolver: Resolvers::UserShow
|
4
|
+
field :public_field, String, null: false, authenticate: false
|
5
|
+
field :private_field, String, null: false, authenticate: true
|
6
|
+
|
7
|
+
def public_field
|
8
|
+
'Field does not require authentication'
|
9
|
+
end
|
10
|
+
|
11
|
+
def private_field
|
12
|
+
'Field will always require authentication'
|
13
|
+
end
|
4
14
|
end
|
5
15
|
end
|
data/spec/dummy/config/routes.rb
CHANGED
@@ -17,6 +17,24 @@ RSpec.describe GraphqlDevise::InstallGenerator, type: :generator do
|
|
17
17
|
run_generator(args)
|
18
18
|
end
|
19
19
|
|
20
|
+
context 'when mount option is schema' do
|
21
|
+
let(:args) { ['Admin', '--mount', 'GqldDummySchema'] }
|
22
|
+
|
23
|
+
it 'mounts the SchemaPlugin' do
|
24
|
+
assert_file 'config/initializers/devise.rb'
|
25
|
+
assert_file 'config/initializers/devise_token_auth.rb', /^\s{2}#{Regexp.escape('config.change_headers_on_each_request = false')}/
|
26
|
+
assert_file 'config/locales/devise.en.yml'
|
27
|
+
|
28
|
+
assert_migration 'db/migrate/devise_token_auth_create_admins.rb'
|
29
|
+
|
30
|
+
assert_file 'app/models/admin.rb', /^\s{2}devise :.+include GraphqlDevise::Concerns::Model/m
|
31
|
+
|
32
|
+
assert_file 'app/controllers/application_controller.rb', /^\s{2}include GraphqlDevise::Concerns::SetUserByToken/
|
33
|
+
|
34
|
+
assert_file 'app/graphql/gqld_dummy_schema.rb', /\s+#{Regexp.escape("GraphqlDevise::ResourceLoader.new('Admin')")}/
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
20
38
|
context 'when passing no params to the generator' do
|
21
39
|
let(:args) { [] }
|
22
40
|
|
@@ -59,5 +77,8 @@ RSpec.describe GraphqlDevise::InstallGenerator, type: :generator do
|
|
59
77
|
FileUtils.cd(File.join(destination_root, '..')) do
|
60
78
|
`rails new gqld_dummy -S -C --skip-action-mailbox --skip-action-text -T --skip-spring --skip-bundle --skip-keeps -G --skip-active-storage -J --skip-listen --skip-bootsnap`
|
61
79
|
end
|
80
|
+
FileUtils.cd(File.join(destination_root, '../gqld_dummy')) do
|
81
|
+
`rails generate graphql:install`
|
82
|
+
end
|
62
83
|
end
|
63
84
|
end
|