graphql_rails 0.5.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) 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 +179 -71
  9. data/docs/README.md +48 -9
  10. data/docs/_sidebar.md +5 -0
  11. data/docs/components/controller.md +294 -8
  12. data/docs/components/decorator.md +69 -0
  13. data/docs/components/model.md +349 -11
  14. data/docs/components/routes.md +43 -1
  15. data/docs/getting_started/quick_start.md +9 -2
  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 +7 -1
  29. data/lib/graphql_rails/attributes.rb +13 -0
  30. data/lib/graphql_rails/{attribute → attributes}/attributable.rb +25 -20
  31. data/lib/graphql_rails/attributes/attribute.rb +94 -0
  32. data/lib/graphql_rails/{attribute → attributes}/attribute_name_parser.rb +2 -2
  33. data/lib/graphql_rails/attributes/input_attribute.rb +65 -0
  34. data/lib/graphql_rails/attributes/input_type_parser.rb +62 -0
  35. data/lib/graphql_rails/attributes/type_name_info.rb +38 -0
  36. data/lib/graphql_rails/attributes/type_parseable.rb +128 -0
  37. data/lib/graphql_rails/attributes/type_parser.rb +121 -0
  38. data/lib/graphql_rails/concerns/service.rb +19 -0
  39. data/lib/graphql_rails/controller.rb +44 -22
  40. data/lib/graphql_rails/controller/action.rb +12 -67
  41. data/lib/graphql_rails/controller/action_configuration.rb +71 -31
  42. data/lib/graphql_rails/controller/build_controller_action_resolver.rb +52 -0
  43. data/lib/graphql_rails/controller/build_controller_action_resolver/controller_action_resolver.rb +28 -0
  44. data/lib/graphql_rails/controller/configuration.rb +58 -4
  45. data/lib/graphql_rails/controller/log_controller_action.rb +66 -0
  46. data/lib/graphql_rails/controller/request.rb +29 -8
  47. data/lib/graphql_rails/controller/request/format_errors.rb +58 -0
  48. data/lib/graphql_rails/decorator.rb +41 -0
  49. data/lib/graphql_rails/decorator/relation_decorator.rb +79 -0
  50. data/lib/graphql_rails/errors/custom_execution_error.rb +22 -0
  51. data/lib/graphql_rails/errors/execution_error.rb +8 -7
  52. data/lib/graphql_rails/errors/system_error.rb +14 -0
  53. data/lib/graphql_rails/errors/validation_error.rb +1 -5
  54. data/lib/graphql_rails/input_configurable.rb +47 -0
  55. data/lib/graphql_rails/integrations.rb +19 -0
  56. data/lib/graphql_rails/integrations/lograge.rb +39 -0
  57. data/lib/graphql_rails/integrations/sentry.rb +34 -0
  58. data/lib/graphql_rails/model.rb +26 -4
  59. data/lib/graphql_rails/model/add_fields_to_graphql_type.rb +45 -0
  60. data/lib/graphql_rails/model/build_connection_type.rb +48 -0
  61. data/lib/graphql_rails/model/{configuration → build_connection_type}/count_items.rb +4 -4
  62. data/lib/graphql_rails/model/build_enum_type.rb +68 -0
  63. data/lib/graphql_rails/model/{graphql_input_type_builder.rb → build_graphql_input_type.rb} +10 -2
  64. data/lib/graphql_rails/model/call_graphql_model_method.rb +72 -0
  65. data/lib/graphql_rails/model/configurable.rb +6 -2
  66. data/lib/graphql_rails/model/configuration.rb +34 -19
  67. data/lib/graphql_rails/model/find_or_build_graphql_type.rb +64 -0
  68. data/lib/graphql_rails/model/find_or_build_graphql_type_class.rb +46 -0
  69. data/lib/graphql_rails/model/input.rb +25 -11
  70. data/lib/graphql_rails/query_runner.rb +68 -0
  71. data/lib/graphql_rails/railtie.rb +10 -0
  72. data/lib/graphql_rails/router.rb +40 -13
  73. data/lib/graphql_rails/router/plain_cursor_encoder.rb +16 -0
  74. data/lib/graphql_rails/router/resource_routes_builder.rb +19 -11
  75. data/lib/graphql_rails/router/route.rb +21 -6
  76. data/lib/graphql_rails/router/schema_builder.rb +29 -11
  77. data/lib/graphql_rails/rspec_controller_helpers.rb +25 -12
  78. data/lib/graphql_rails/tasks/dump_graphql_schema.rb +57 -0
  79. data/lib/graphql_rails/tasks/schema.rake +14 -0
  80. data/lib/graphql_rails/version.rb +1 -1
  81. metadata +75 -22
  82. data/README.md +0 -194
  83. data/lib/graphql_rails/attribute.rb +0 -28
  84. data/lib/graphql_rails/attribute/type_parser.rb +0 -115
  85. data/lib/graphql_rails/controller/controller_function.rb +0 -50
  86. data/lib/graphql_rails/controller/format_results.rb +0 -36
  87. data/lib/graphql_rails/model/graphql_type_builder.rb +0 -33
  88. data/lib/graphql_rails/model/input_attribute.rb +0 -47
@@ -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
@@ -1,15 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support/core_ext/string/inflections'
4
- require 'graphql_rails/attribute'
5
4
  require 'graphql_rails/controller/action_configuration'
6
5
  require 'graphql_rails/controller/action_hook'
6
+ require 'graphql_rails/errors/error'
7
7
 
8
8
  module GraphqlRails
9
9
  class Controller
10
10
  # stores all graphql_rails contoller specific config
11
11
  class Configuration
12
- def initialize
12
+ class InvalidActionConfiguration < GraphqlRails::Error; end
13
+
14
+ LIB_REGEXP = %r{/graphql_rails/lib/}
15
+
16
+ attr_reader :action_by_name
17
+
18
+ def initialize(controller)
19
+ @controller = controller
13
20
  @hooks = {
14
21
  before: {},
15
22
  after: {},
@@ -17,6 +24,7 @@ module GraphqlRails
17
24
  }
18
25
 
19
26
  @action_by_name = {}
27
+ @action_default = nil
20
28
  end
21
29
 
22
30
  def initialize_copy(other)
@@ -30,6 +38,12 @@ module GraphqlRails
30
38
  end
31
39
  end
32
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
+
33
47
  def action_hooks_for(hook_type, action_name)
34
48
  hooks[hook_type].values.select { |hook| hook.applicable_for?(action_name) }
35
49
  end
@@ -42,13 +56,53 @@ module GraphqlRails
42
56
  ActionHook.new(name: hook_name, **options, &block)
43
57
  end
44
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
+
45
65
  def action(method_name)
46
- @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)
47
83
  end
48
84
 
49
85
  private
50
86
 
51
- 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
52
106
  end
53
107
  end
54
108
  end
@@ -0,0 +1,66 @@
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
+ parametter_filter = ActionDispatch::Http::ParameterFilter.new(filter_options)
55
+ parametter_filter.filter(params)
56
+ end
57
+ end
58
+
59
+ def filter_parameters
60
+ return [] if !defined?(Rails) || Rails.application.nil?
61
+
62
+ Rails.application.config.filter_parameters || []
63
+ end
64
+ end
65
+ end
66
+ 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,79 @@
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
+
75
+ GraphQL::Relay::BaseConnection.register_connection_implementation(
76
+ RelationDecorator, GraphQL::Relay::RelationConnection
77
+ )
78
+ end
79
+ 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