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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/CHANGELOG.md +8 -0
  4. data/README.md +187 -20
  5. data/app/controllers/graphql_devise/application_controller.rb +4 -5
  6. data/app/controllers/graphql_devise/concerns/set_user_by_token.rb +23 -0
  7. data/app/controllers/graphql_devise/graphql_controller.rb +2 -0
  8. data/app/helpers/graphql_devise/mailer_helper.rb +2 -2
  9. data/config/routes.rb +2 -0
  10. data/graphql_devise.gemspec +3 -3
  11. data/lib/generators/graphql_devise/install_generator.rb +28 -5
  12. data/lib/graphql_devise.rb +16 -4
  13. data/lib/graphql_devise/mount_method/operation_preparer.rb +2 -2
  14. data/lib/graphql_devise/mount_method/operation_preparers/resource_name_setter.rb +1 -1
  15. data/lib/graphql_devise/mutations/sign_up.rb +1 -5
  16. data/lib/graphql_devise/rails/routes.rb +5 -72
  17. data/lib/graphql_devise/resource_loader.rb +87 -0
  18. data/lib/graphql_devise/schema_plugin.rb +87 -0
  19. data/lib/graphql_devise/version.rb +1 -1
  20. data/spec/dummy/app/controllers/api/v1/graphql_controller.rb +6 -2
  21. data/spec/dummy/app/graphql/dummy_schema.rb +9 -0
  22. data/spec/dummy/app/graphql/interpreter_schema.rb +9 -0
  23. data/spec/dummy/app/graphql/types/mutation_type.rb +1 -1
  24. data/spec/dummy/app/graphql/types/query_type.rb +10 -0
  25. data/spec/dummy/config/routes.rb +1 -0
  26. data/spec/generators/graphql_devise/install_generator_spec.rb +21 -0
  27. data/spec/rails_helper.rb +0 -1
  28. data/spec/requests/graphql_controller_spec.rb +80 -0
  29. data/spec/requests/user_controller_spec.rb +180 -24
  30. data/spec/services/mount_method/operation_preparer_spec.rb +2 -2
  31. data/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb +1 -1
  32. data/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb +1 -1
  33. data/spec/services/mount_method/operation_preparers/resource_name_setter_spec.rb +1 -1
  34. data/spec/services/resource_loader_spec.rb +82 -0
  35. data/spec/services/schema_plugin_spec.rb +26 -0
  36. metadata +31 -5
  37. data/spec/support/generators/file_helpers.rb +0 -12
@@ -18,12 +18,21 @@ module GraphqlDevise
18
18
  @schema_loaded = true
19
19
  end
20
20
 
21
- def self.mount_resource(resource)
22
- @mounted_resources << resource
21
+ def self.resource_mounted?(mapping_name)
22
+ @mounted_resources.include?(mapping_name)
23
23
  end
24
24
 
25
- def self.resource_mounted?(resource)
26
- @mounted_resources.include?(resource)
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(resource:, selected_operations:, preparer:, custom:, additional_operations:)
11
+ def initialize(mapping_name:, selected_operations:, preparer:, custom:, additional_operations:)
12
12
  @selected_operations = selected_operations
13
13
  @preparer = preparer
14
- @mapping_name = resource.underscore.tr('/', '_')
14
+ @mapping_name = mapping_name
15
15
  @custom = custom
16
16
  @additional_operations = additional_operations
17
17
  end
@@ -7,7 +7,7 @@ module GraphqlDevise
7
7
  end
8
8
 
9
9
  def call(operation)
10
- operation.instance_variable_set(:@resource_name, @name.to_sym)
10
+ operation.instance_variable_set(:@resource_name, @name)
11
11
 
12
12
  operation
13
13
  end
@@ -35,7 +35,7 @@ module GraphqlDevise
35
35
 
36
36
  { authenticatable: resource }
37
37
  else
38
- clean_up_passwords(resource)
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
- default_operations = GraphqlDevise::DefaultOperations::MUTATIONS.merge(GraphqlDevise::DefaultOperations::QUERIES)
15
-
16
- # clean_options responds to all keys defined in GraphqlDevise::MountMethod::SUPPORTED_OPTIONS
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
- devise_scope resource.underscore.tr('/', '_').to_sym do
36
- post clean_options.at, to: 'graphql_devise/graphql#auth'
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)
@@ -1,3 +1,3 @@
1
1
  module GraphqlDevise
2
- VERSION = '0.11.4'.freeze
2
+ VERSION = '0.12.0'.freeze
3
3
  end
@@ -3,10 +3,14 @@ module Api
3
3
  class GraphqlController < ApplicationController
4
4
  include GraphqlDevise::Concerns::SetUserByToken
5
5
 
6
- before_action :authenticate_user!
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,6 +1,6 @@
1
1
  module Types
2
2
  class MutationType < Types::BaseObject
3
- field :dummy_mutation, String, null: false
3
+ field :dummy_mutation, String, null: false, authenticate: true
4
4
 
5
5
  def dummy_mutation
6
6
  'Necessary so GraphQL gem does not complain about empty mutation type'
@@ -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
@@ -28,4 +28,5 @@ Rails.application.routes.draw do
28
28
  )
29
29
 
30
30
  post '/api/v1/graphql', to: 'api/v1/graphql#graphql'
31
+ post '/api/v1/interpreter', to: 'api/v1/graphql#interpreter'
31
32
  end
@@ -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