graphql_rails 0.7.0 → 1.2.1

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.hound.yml +1 -0
  3. data/.rubocop.yml +3 -3
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +2 -2
  6. data/CHANGELOG.md +35 -0
  7. data/Gemfile +3 -2
  8. data/Gemfile.lock +181 -71
  9. data/docs/README.md +40 -8
  10. data/docs/_sidebar.md +5 -0
  11. data/docs/components/controller.md +295 -9
  12. data/docs/components/decorator.md +69 -0
  13. data/docs/components/model.md +267 -6
  14. data/docs/components/routes.md +28 -0
  15. data/docs/getting_started/quick_start.md +10 -3
  16. data/docs/index.html +1 -1
  17. data/docs/logging_and_monitoring/logging_and_monitoring.md +35 -0
  18. data/docs/other_tools/query_runner.md +49 -0
  19. data/docs/other_tools/schema_dump.md +29 -0
  20. data/docs/testing/testing.md +3 -1
  21. data/graphql_rails.gemspec +5 -4
  22. data/lib/generators/graphql_rails/install_generator.rb +50 -0
  23. data/lib/generators/graphql_rails/templates/example_users_controller.erb +19 -0
  24. data/lib/generators/graphql_rails/templates/graphql_application_controller.erb +8 -0
  25. data/lib/generators/graphql_rails/templates/graphql_controller.erb +20 -0
  26. data/lib/generators/graphql_rails/templates/graphql_router.erb +19 -0
  27. data/lib/generators/graphql_rails/templates/graphql_router_spec.erb +21 -0
  28. data/lib/graphql_rails.rb +6 -0
  29. data/lib/graphql_rails/attributes/attributable.rb +22 -17
  30. data/lib/graphql_rails/attributes/attribute.rb +67 -3
  31. data/lib/graphql_rails/attributes/attribute_name_parser.rb +4 -4
  32. data/lib/graphql_rails/attributes/input_attribute.rb +33 -15
  33. data/lib/graphql_rails/attributes/input_type_parser.rb +62 -0
  34. data/lib/graphql_rails/attributes/type_name_info.rb +38 -0
  35. data/lib/graphql_rails/attributes/type_parseable.rb +132 -0
  36. data/lib/graphql_rails/attributes/type_parser.rb +59 -53
  37. data/lib/graphql_rails/concerns/service.rb +19 -0
  38. data/lib/graphql_rails/controller.rb +42 -21
  39. data/lib/graphql_rails/controller/action.rb +12 -67
  40. data/lib/graphql_rails/controller/action_configuration.rb +70 -28
  41. data/lib/graphql_rails/controller/build_controller_action_resolver.rb +52 -0
  42. data/lib/graphql_rails/controller/build_controller_action_resolver/controller_action_resolver.rb +28 -0
  43. data/lib/graphql_rails/controller/configuration.rb +56 -3
  44. data/lib/graphql_rails/controller/log_controller_action.rb +71 -0
  45. data/lib/graphql_rails/controller/request.rb +29 -8
  46. data/lib/graphql_rails/controller/request/format_errors.rb +58 -0
  47. data/lib/graphql_rails/decorator.rb +41 -0
  48. data/lib/graphql_rails/decorator/relation_decorator.rb +75 -0
  49. data/lib/graphql_rails/errors/custom_execution_error.rb +22 -0
  50. data/lib/graphql_rails/errors/execution_error.rb +6 -7
  51. data/lib/graphql_rails/errors/system_error.rb +14 -0
  52. data/lib/graphql_rails/errors/validation_error.rb +1 -5
  53. data/lib/graphql_rails/input_configurable.rb +47 -0
  54. data/lib/graphql_rails/integrations.rb +19 -0
  55. data/lib/graphql_rails/integrations/lograge.rb +39 -0
  56. data/lib/graphql_rails/integrations/sentry.rb +34 -0
  57. data/lib/graphql_rails/model.rb +26 -4
  58. data/lib/graphql_rails/model/add_fields_to_graphql_type.rb +45 -0
  59. data/lib/graphql_rails/model/build_connection_type.rb +52 -0
  60. data/lib/graphql_rails/model/{configuration → build_connection_type}/count_items.rb +5 -5
  61. data/lib/graphql_rails/model/build_enum_type.rb +39 -10
  62. data/lib/graphql_rails/model/build_graphql_input_type.rb +8 -4
  63. data/lib/graphql_rails/model/call_graphql_model_method.rb +72 -0
  64. data/lib/graphql_rails/model/configurable.rb +6 -2
  65. data/lib/graphql_rails/model/configuration.rb +30 -16
  66. data/lib/graphql_rails/model/find_or_build_graphql_type.rb +64 -0
  67. data/lib/graphql_rails/model/find_or_build_graphql_type_class.rb +46 -0
  68. data/lib/graphql_rails/model/input.rb +11 -7
  69. data/lib/graphql_rails/query_runner.rb +68 -0
  70. data/lib/graphql_rails/railtie.rb +10 -0
  71. data/lib/graphql_rails/router.rb +40 -13
  72. data/lib/graphql_rails/router/resource_routes_builder.rb +10 -9
  73. data/lib/graphql_rails/router/route.rb +21 -6
  74. data/lib/graphql_rails/router/schema_builder.rb +30 -11
  75. data/lib/graphql_rails/rspec_controller_helpers.rb +6 -4
  76. data/lib/graphql_rails/tasks/dump_graphql_schema.rb +57 -0
  77. data/lib/graphql_rails/tasks/schema.rake +14 -0
  78. data/lib/graphql_rails/version.rb +1 -1
  79. metadata +70 -19
  80. data/lib/graphql_rails/controller/controller_function.rb +0 -50
  81. data/lib/graphql_rails/controller/format_results.rb +0 -36
  82. data/lib/graphql_rails/model/build_graphql_type.rb +0 -37
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql_rails/controller/request'
4
+
5
+ module GraphqlRails
6
+ class Controller
7
+ class BuildControllerActionResolver
8
+ # Resolver which includes controller specific methods.
9
+ # Used to simplify resolver build for each controller action
10
+ class ControllerActionResolver < GraphQL::Schema::Resolver
11
+ def self.controller(controller_class = nil)
12
+ @controller = controller_class if controller_class
13
+ @controller
14
+ end
15
+
16
+ def self.controller_action_name(name = nil)
17
+ @controller_action_name = name if name
18
+ @controller_action_name
19
+ end
20
+
21
+ def resolve(**inputs)
22
+ request = Request.new(object, inputs, context)
23
+ self.class.controller.new(request).call(self.class.controller_action_name)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -3,14 +3,20 @@
3
3
  require 'active_support/core_ext/string/inflections'
4
4
  require 'graphql_rails/controller/action_configuration'
5
5
  require 'graphql_rails/controller/action_hook'
6
+ require 'graphql_rails/errors/error'
6
7
 
7
8
  module GraphqlRails
8
9
  class Controller
9
10
  # stores all graphql_rails contoller specific config
10
11
  class Configuration
12
+ class InvalidActionConfiguration < GraphqlRails::Error; end
13
+
14
+ LIB_REGEXP = %r{/graphql_rails/lib/}
15
+
11
16
  attr_reader :action_by_name
12
17
 
13
- def initialize
18
+ def initialize(controller)
19
+ @controller = controller
14
20
  @hooks = {
15
21
  before: {},
16
22
  after: {},
@@ -18,6 +24,7 @@ module GraphqlRails
18
24
  }
19
25
 
20
26
  @action_by_name = {}
27
+ @action_default = nil
21
28
  end
22
29
 
23
30
  def initialize_copy(other)
@@ -31,6 +38,12 @@ module GraphqlRails
31
38
  end
32
39
  end
33
40
 
41
+ def dup_with(controller:)
42
+ dup.tap do |new_config|
43
+ new_config.instance_variable_set(:@controller, controller)
44
+ end
45
+ end
46
+
34
47
  def action_hooks_for(hook_type, action_name)
35
48
  hooks[hook_type].values.select { |hook| hook.applicable_for?(action_name) }
36
49
  end
@@ -43,13 +56,53 @@ module GraphqlRails
43
56
  ActionHook.new(name: hook_name, **options, &block)
44
57
  end
45
58
 
59
+ def action_default
60
+ @action_default ||= ActionConfiguration.new(name: :default, controller: nil)
61
+ yield(@action_default) if block_given?
62
+ @action_default
63
+ end
64
+
46
65
  def action(method_name)
47
- @action_by_name[method_name.to_s] ||= ActionConfiguration.new
66
+ action_name = method_name.to_s.underscore
67
+ @action_by_name[action_name] ||= action_default.dup_with(
68
+ name: action_name,
69
+ controller: controller,
70
+ defined_at: dynamic_source_location
71
+ )
72
+ yield(@action_by_name[action_name]) if block_given?
73
+ @action_by_name[action_name]
74
+ end
75
+
76
+ def action_config(method_name)
77
+ action_name = method_name.to_s.underscore
78
+ @action_by_name.fetch(action_name) { raise_invalid_config_error(action_name) }
79
+ end
80
+
81
+ def model(model = nil)
82
+ action_default.model(model)
48
83
  end
49
84
 
50
85
  private
51
86
 
52
- attr_reader :hooks
87
+ attr_reader :hooks, :controller
88
+
89
+ def dynamic_source_location
90
+ project_trace = \
91
+ caller
92
+ .dup
93
+ .drop_while { |path| !path.match?(LIB_REGEXP) }
94
+ .drop_while { |path| path.match?(LIB_REGEXP) }
95
+
96
+ project_trace.first
97
+ end
98
+
99
+ def raise_invalid_config_error(action_name)
100
+ error_message = \
101
+ "Missing action configuration for #{controller}##{action_name}. " \
102
+ "Please define it with `action(:#{action_name})`."
103
+
104
+ raise InvalidActionConfiguration, error_message
105
+ end
53
106
  end
54
107
  end
55
108
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql_rails/errors/execution_error'
4
+
5
+ module GraphqlRails
6
+ class Controller
7
+ # logs controller start and end times
8
+ class LogControllerAction
9
+ require 'graphql_rails/concerns/service'
10
+
11
+ include ::GraphqlRails::Service
12
+
13
+ START_PROCESSING_KEY = 'start_processing.graphql_action_controller'
14
+ PROCESS_ACTION_KEY = 'process_action.graphql_action_controller'
15
+
16
+ def initialize(controller_name:, action_name:, params:, graphql_request:)
17
+ @controller_name = controller_name
18
+ @action_name = action_name
19
+ @params = params
20
+ @graphql_request = graphql_request
21
+ end
22
+
23
+ def call
24
+ ActiveSupport::Notifications.instrument(START_PROCESSING_KEY, default_payload)
25
+ ActiveSupport::Notifications.instrument(PROCESS_ACTION_KEY, default_payload) do |payload|
26
+ yield.tap do
27
+ payload[:status] = status
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :controller_name, :action_name, :params, :graphql_request
35
+
36
+ def default_payload
37
+ {
38
+ controller: controller_name,
39
+ action: action_name,
40
+ params: filtered_params
41
+ }
42
+ end
43
+
44
+ def status
45
+ graphql_request.errors.present? ? 500 : 200
46
+ end
47
+
48
+ def filtered_params
49
+ @filtered_params ||=
50
+ if filter_parameters.empty?
51
+ params
52
+ else
53
+ filter_options = Rails.configuration.filter_parameters
54
+ parameter_filter_class.new(filter_options).filter(params)
55
+ end
56
+ end
57
+
58
+ def filter_parameters
59
+ return [] if !defined?(Rails) || Rails.application.nil?
60
+
61
+ Rails.application.config.filter_parameters || []
62
+ end
63
+
64
+ def parameter_filter_class
65
+ return ActiveSupport::ParameterFilter if Object.const_defined?('ActiveSupport::ParameterFilter')
66
+
67
+ ActionDispatch::Http::ParameterFilter
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'graphql_rails/errors/execution_error'
4
-
5
3
  module GraphqlRails
6
4
  class Controller
7
5
  # Contains all info related with single request to controller
8
6
  class Request
7
+ require 'graphql_rails/controller/request/format_errors'
8
+
9
9
  attr_accessor :object_to_return
10
10
  attr_reader :errors, :context
11
11
 
@@ -16,12 +16,9 @@ module GraphqlRails
16
16
  end
17
17
 
18
18
  def errors=(new_errors)
19
- @errors = new_errors
19
+ @errors = FormatErrors.call(not_formatted_errors: new_errors)
20
20
 
21
- new_errors.each do |error|
22
- error_message = error.is_a?(String) ? error : error.message
23
- context.add_error(ExecutionError.new(error_message))
24
- end
21
+ @errors.each { |error| context.add_error(error) }
25
22
  end
26
23
 
27
24
  def no_object_to_return?
@@ -29,12 +26,36 @@ module GraphqlRails
29
26
  end
30
27
 
31
28
  def params
32
- inputs.to_h
29
+ deep_transform_values(inputs.to_h) do |val|
30
+ graphql_object_to_hash(val)
31
+ end
33
32
  end
34
33
 
35
34
  private
36
35
 
37
36
  attr_reader :graphql_object, :inputs
37
+
38
+ def graphql_object_to_hash(object)
39
+ if object.is_a?(GraphQL::Dig)
40
+ object.to_h
41
+ elsif object.is_a?(Array)
42
+ object.map { |item| graphql_object_to_hash(item) }
43
+ else
44
+ object
45
+ end
46
+ end
47
+
48
+ def deep_transform_values(hash, &block)
49
+ return hash unless hash.is_a?(Hash)
50
+
51
+ hash.transform_values do |val|
52
+ if val.is_a?(Hash)
53
+ deep_transform_values(val, &block)
54
+ else
55
+ yield(val)
56
+ end
57
+ end
58
+ end
38
59
  end
39
60
  end
40
61
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql_rails/concerns/service'
4
+ require 'graphql_rails/errors/execution_error'
5
+ require 'graphql_rails/errors/validation_error'
6
+ require 'graphql_rails/errors/custom_execution_error'
7
+
8
+ module GraphqlRails
9
+ class Controller
10
+ class Request
11
+ # Converts user provided free-form errors in to meaningfull graphql error classes
12
+ class FormatErrors
13
+ include Service
14
+
15
+ def initialize(not_formatted_errors:)
16
+ @not_formatted_errors = not_formatted_errors
17
+ end
18
+
19
+ def call
20
+ if validation_errors?
21
+ formatted_validation_errors
22
+ else
23
+ not_formatted_errors.map { |error| format_error(error) }
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :not_formatted_errors
30
+
31
+ def validation_errors?
32
+ defined?(ActiveModel) &&
33
+ defined?(ActiveModel::Errors) &&
34
+ not_formatted_errors.is_a?(ActiveModel::Errors)
35
+ end
36
+
37
+ def formatted_validation_errors
38
+ not_formatted_errors.map do |field, message|
39
+ ValidationError.new(message, field)
40
+ end
41
+ end
42
+
43
+ def format_error(error)
44
+ if error.is_a?(String)
45
+ ExecutionError.new(error)
46
+ elsif error.is_a?(GraphQL::ExecutionError)
47
+ error
48
+ elsif CustomExecutionError.accepts?(error)
49
+ message = error[:message] || error['message']
50
+ CustomExecutionError.new(message, error.except(:message, 'message'))
51
+ elsif error.respond_to?(:message)
52
+ ExecutionError.new(error.message)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlRails
4
+ # adds `.decorate` class method to any class. Handy when using with paginated responses
5
+ #
6
+ # usage:
7
+ # class FriendDecorator < SimpleDecorator
8
+ # include GraphqlRails::Decorator
9
+ #
10
+ # graphql.attribute :full_name
11
+ # end
12
+ #
13
+ # class User
14
+ # has_many :friends
15
+ # graphql.attribute :decorated_friends, paginated: true, type: 'FriendDecorator!'
16
+ #
17
+ # def decorated_friends
18
+ # FriendDecorator.decorate(friends)
19
+ # end
20
+ # end
21
+ module Decorator
22
+ require 'active_support/concern'
23
+ require 'graphql_rails/decorator/relation_decorator'
24
+
25
+ extend ActiveSupport::Concern
26
+
27
+ class_methods do
28
+ def decorate(object, *args)
29
+ if Decorator::RelationDecorator.decorates?(object)
30
+ Decorator::RelationDecorator.new(relation: object, decorator: self, decorator_args: args)
31
+ elsif object.nil?
32
+ nil
33
+ elsif object.is_a?(Array)
34
+ object.map { |item| new(item, *args) }
35
+ else
36
+ new(object, *args)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlRails
4
+ module Decorator
5
+ # wrapps active record relation and returns decorated object instead
6
+ class RelationDecorator
7
+ delegate :map, :each, to: :to_a
8
+ delegate :limit_value, :offset_value, :count, :size, to: :relation
9
+
10
+ def self.decorates?(object)
11
+ (defined?(ActiveRecord) && object.is_a?(ActiveRecord::Relation)) ||
12
+ defined?(Mongoid) && object.is_a?(Mongoid::Criteria)
13
+ end
14
+
15
+ def initialize(decorator:, relation:, decorator_args: [])
16
+ @relation = relation
17
+ @decorator = decorator
18
+ @decorator_args = decorator_args
19
+ end
20
+
21
+ %i[where limit order group offset from select having all unscope].each do |method_name|
22
+ define_method method_name do |*args, &block|
23
+ chainable_method(method_name, *args, &block)
24
+ end
25
+ end
26
+
27
+ %i[first second last].each do |method_name|
28
+ define_method method_name do |*args, &block|
29
+ decoratable_object_method(method_name, *args, &block)
30
+ end
31
+ end
32
+
33
+ %i[find_each].each do |method_name|
34
+ define_method method_name do |*args, &block|
35
+ decoratable_block_method(method_name, *args, &block)
36
+ end
37
+ end
38
+
39
+ def to_a
40
+ @to_a ||= relation.to_a.map { |it| decorator.new(it, *decorator_args) }
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :relation, :decorator, :decorator_args
46
+
47
+ def decoratable_object_method(method_name, *args, &block)
48
+ object = relation.public_send(method_name, *args, &block)
49
+ decorate(object)
50
+ end
51
+
52
+ def decorate(object_or_list)
53
+ return object_or_list if object_or_list.blank?
54
+
55
+ if object_or_list.is_a?(Array)
56
+ object_or_list.map { |it| decorator.new(it, *decorator_args) }
57
+ else
58
+ decorator.new(object_or_list, *decorator_args)
59
+ end
60
+ end
61
+
62
+ def decoratable_block_method(method_name, *args)
63
+ relation.public_send(method_name, *args) do |object, *other_args|
64
+ decorated_object = decorate(object)
65
+ yield(decorated_object, *other_args)
66
+ end
67
+ end
68
+
69
+ def chainable_method(method_name, *args, &block)
70
+ new_relation = relation.public_send(method_name, *args, &block)
71
+ self.class.new(decorator: decorator, relation: new_relation, decorator_args: decorator_args)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlRails
4
+ # base class which is returned in case something bad happens. Contains all error rendering tructure
5
+ class CustomExecutionError < ExecutionError
6
+ attr_reader :extra_graphql_data
7
+
8
+ def self.accepts?(error)
9
+ error.is_a?(Hash) &&
10
+ (error.key?(:message) || error.key?('message'))
11
+ end
12
+
13
+ def initialize(message, extra_graphql_data = {})
14
+ super(message)
15
+ @extra_graphql_data = extra_graphql_data.stringify_keys
16
+ end
17
+
18
+ def to_h
19
+ super
20
+ end
21
+ end
22
+ end