graphql_rails 0.7.0 → 0.8.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.
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql'
4
+
5
+ module GraphqlRails
6
+ module Attributes
7
+ # converts string value in to GraphQL type
8
+ class InputTypeParser < TypeParser
9
+ def initialize(unparsed_type, subtype:)
10
+ super(unparsed_type)
11
+ @subtype = subtype
12
+ end
13
+
14
+ def graphql_type
15
+ return nil if unparsed_type.nil?
16
+
17
+ partly_parsed_type || parsed_type
18
+ end
19
+
20
+ def nullable_type
21
+ return nil if unparsed_type.nil?
22
+
23
+ partly_parsed_type || parsed_nullable_type
24
+ end
25
+
26
+ protected
27
+
28
+ def partly_parsed_type
29
+ return unparsed_type if raw_graphql_type?
30
+ return unparsed_type.graphql_input_type if unparsed_type.is_a?(GraphqlRails::Model::Input)
31
+ end
32
+
33
+ def parsed_nullable_type
34
+ if list?
35
+ parsed_inner_type.to_list_type
36
+ else
37
+ type_by_name
38
+ end
39
+ end
40
+
41
+ def parsed_type
42
+ if list?
43
+ parsed_list_type
44
+ else
45
+ parsed_inner_type
46
+ end
47
+ end
48
+
49
+ def raw_graphql_type?
50
+ unparsed_type.is_a?(GraphQL::InputObjectType) || super
51
+ end
52
+
53
+ def dynamicly_defined_type
54
+ type_class = graphql_model
55
+ return unless type_class
56
+
57
+ type_class.graphql.input(*subtype).graphql_input_type
58
+ end
59
+
60
+ def parsed_list_type
61
+ list_type = parsed_inner_type.to_list_type
62
+
63
+ if required_list?
64
+ list_type = list_type.to_graphql if list_type.respond_to?(:to_graphql)
65
+ list_type.to_non_null_type
66
+ else
67
+ list_type
68
+ end
69
+ end
70
+
71
+ def parsed_inner_type
72
+ if required_inner_type?
73
+ type_by_name.to_non_null_type
74
+ else
75
+ type_by_name
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ attr_reader :subtype
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlRails
4
+ module Attributes
5
+ # checks various attributes based on graphql type name
6
+ class TypeNameInfo
7
+ attr_reader :name
8
+
9
+ def initialize(name)
10
+ @name = name
11
+ end
12
+
13
+ def nullable_inner_name
14
+ inner_name[/[^!]+/]
15
+ end
16
+
17
+ def inner_name
18
+ name[/[^!\[\]]+!?/]
19
+ end
20
+
21
+ def required_inner_type?
22
+ inner_name.include?('!')
23
+ end
24
+
25
+ def list?
26
+ name.include?(']')
27
+ end
28
+
29
+ def required?
30
+ name.end_with?('!')
31
+ end
32
+
33
+ def required_list?
34
+ required? && list?
35
+ end
36
+ end
37
+ end
38
+ end
@@ -6,6 +6,8 @@ module GraphqlRails
6
6
  module Attributes
7
7
  # converts string value in to GraphQL type
8
8
  class TypeParser
9
+ require_relative './type_name_info'
10
+
9
11
  class UnknownTypeError < ArgumentError; end
10
12
 
11
13
  TYPE_MAPPING = {
@@ -28,6 +30,13 @@ module GraphqlRails
28
30
  'decimal' => GraphQL::FLOAT_TYPE
29
31
  }.freeze
30
32
 
33
+ RAW_GRAPHQL_TYPES = [
34
+ GraphQL::Schema::List,
35
+ GraphQL::BaseType,
36
+ GraphQL::ObjectType,
37
+ GraphQL::InputObjectType
38
+ ].freeze
39
+
31
40
  def initialize(unparsed_type)
32
41
  @unparsed_type = unparsed_type
33
42
  end
@@ -43,20 +52,31 @@ module GraphqlRails
43
52
  end
44
53
 
45
54
  def graphql_model
46
- type_class = inner_type_name.safe_constantize
55
+ type_class = nullable_inner_name.safe_constantize
47
56
  return unless type_class.respond_to?(:graphql)
48
57
 
49
58
  type_class
50
59
  end
51
60
 
61
+ protected
62
+
63
+ def dynamicly_defined_type
64
+ type_class = graphql_model
65
+ return unless type_class
66
+
67
+ type_class.graphql.graphql_type
68
+ end
69
+
52
70
  private
53
71
 
72
+ delegate :list?, :required_inner_type?, :required_list?, :nullable_inner_name, to: :type_name_info
73
+
54
74
  attr_reader :unparsed_type
55
75
 
56
76
  def parsed_list_type
57
77
  list_type = parsed_inner_type.to_list_type
58
78
 
59
- if required_list_type?
79
+ if required_list?
60
80
  list_type.to_non_null_type
61
81
  else
62
82
  list_type
@@ -71,31 +91,20 @@ module GraphqlRails
71
91
  end
72
92
  end
73
93
 
74
- def required_inner_type?
75
- !!unparsed_type[/\w!/] # rubocop:disable Style/DoubleNegation
76
- end
77
-
78
- def list?
79
- unparsed_type.to_s.include?(']')
80
- end
81
-
82
- def required_list_type?
83
- unparsed_type.to_s.include?(']!')
84
- end
85
-
86
94
  def raw_graphql_type?
87
- unparsed_type.is_a?(GraphQL::BaseType) ||
88
- unparsed_type.is_a?(GraphQL::ObjectType) ||
89
- unparsed_type.is_a?(GraphQL::InputObjectType) ||
90
- (defined?(GraphQL::Schema::Member) && unparsed_type.is_a?(Class) && unparsed_type < GraphQL::Schema::Member)
95
+ return true if RAW_GRAPHQL_TYPES.detect { |raw_type| unparsed_type.is_a?(raw_type) }
96
+
97
+ defined?(GraphQL::Schema::Member) &&
98
+ unparsed_type.is_a?(Class) &&
99
+ unparsed_type < GraphQL::Schema::Member
91
100
  end
92
101
 
93
- def inner_type_name
94
- unparsed_type.to_s.tr('[]!', '')
102
+ def type_name_info
103
+ @type_name_info ||= TypeNameInfo.new(unparsed_type.to_s)
95
104
  end
96
105
 
97
106
  def type_by_name
98
- TYPE_MAPPING.fetch(inner_type_name.downcase) do
107
+ TYPE_MAPPING.fetch(nullable_inner_name.downcase) do
99
108
  dynamicly_defined_type || raise(
100
109
  UnknownTypeError,
101
110
  "Type #{unparsed_type.inspect} is not supported. Supported scalar types are: #{TYPE_MAPPING.keys}." \
@@ -103,13 +112,6 @@ module GraphqlRails
103
112
  )
104
113
  end
105
114
  end
106
-
107
- def dynamicly_defined_type
108
- type_class = graphql_model
109
- return unless type_class
110
-
111
- type_class.graphql.graphql_type
112
- end
113
115
  end
114
116
  end
115
117
  end
@@ -26,8 +26,19 @@ module GraphqlRails
26
26
  end
27
27
 
28
28
  def permit(*no_type_attributes, **typed_attributes)
29
- no_type_attributes.each { |attribute| permit_attribute(attribute) }
30
- typed_attributes.each { |attribute, type| permit_attribute(attribute, type) }
29
+ no_type_attributes.each { |attribute| permit_input(attribute) }
30
+ typed_attributes.each { |attribute, type| permit_input(attribute, type: type) }
31
+ self
32
+ end
33
+
34
+ def permit_input(name, type: nil, options: {}, **input_options)
35
+ field_name = name.to_s.remove(/!\Z/)
36
+
37
+ attributes[field_name] = Attributes::InputAttribute.new(
38
+ name.to_s, type,
39
+ options: action_options.merge(options),
40
+ **input_options
41
+ )
31
42
  self
32
43
  end
33
44
 
@@ -86,11 +97,6 @@ module GraphqlRails
86
97
  def type_parser
87
98
  Attributes::TypeParser.new(custom_return_type)
88
99
  end
89
-
90
- def permit_attribute(name, type = nil)
91
- field_name = name.to_s.remove(/!\Z/)
92
- attributes[field_name] = Attributes::InputAttribute.new(name.to_s, type, options: action_options)
93
- end
94
100
  end
95
101
  end
96
102
  end
@@ -45,6 +45,8 @@ module GraphqlRails
45
45
 
46
46
  def action(method_name)
47
47
  @action_by_name[method_name.to_s] ||= ActionConfiguration.new
48
+ yield(@action_by_name[method_name.to_s]) if block_given?
49
+ @action_by_name[method_name.to_s]
48
50
  end
49
51
 
50
52
  private
@@ -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
+ START_PROCESSING_KEY = 'start_processing.graphql_action_controller'
10
+ PROCESS_ACTION_KEY = 'process_action.graphql_action_controller'
11
+
12
+ def self.call(**kwargs, &block)
13
+ new(**kwargs).call(&block)
14
+ end
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
@@ -6,12 +6,14 @@ require 'graphql_rails/controller/configuration'
6
6
  require 'graphql_rails/controller/request'
7
7
  require 'graphql_rails/controller/format_results'
8
8
  require 'graphql_rails/controller/action_hooks_runner'
9
+ require 'graphql_rails/controller/log_controller_action'
9
10
 
10
11
  module GraphqlRails
11
12
  # base class for all graphql_rails controllers
12
13
  class Controller
13
14
  class << self
14
15
  def inherited(sublass)
16
+ super
15
17
  sublass.instance_variable_set(:@controller_configuration, controller_configuration.dup)
16
18
  end
17
19
 
@@ -44,14 +46,10 @@ module GraphqlRails
44
46
 
45
47
  def call(method_name)
46
48
  @action_name = method_name
47
- call_with_rendering(method_name)
48
-
49
- FormatResults.new(
50
- graphql_request.object_to_return,
51
- action_config: self.class.action(method_name),
52
- params: params,
53
- graphql_context: graphql_request.context
54
- ).call
49
+ with_controller_action_logging do
50
+ call_with_rendering
51
+ format_controller_results
52
+ end
55
53
  ensure
56
54
  @action_name = nil
57
55
  end
@@ -74,7 +72,7 @@ module GraphqlRails
74
72
 
75
73
  private
76
74
 
77
- def call_with_rendering(action_name)
75
+ def call_with_rendering
78
76
  hooks_runner = ActionHooksRunner.new(action_name: action_name, controller: self)
79
77
  response = hooks_runner.call { public_send(action_name) }
80
78
 
@@ -90,5 +88,24 @@ module GraphqlRails
90
88
  errors = rendering_params[:error] || rendering_params[:errors]
91
89
  Array(errors)
92
90
  end
91
+
92
+ def with_controller_action_logging(&block)
93
+ LogControllerAction.call(
94
+ controller_name: self.class.name,
95
+ action_name: action_name,
96
+ params: params,
97
+ graphql_request: graphql_request,
98
+ &block
99
+ )
100
+ end
101
+
102
+ def format_controller_results
103
+ FormatResults.new(
104
+ graphql_request.object_to_return,
105
+ action_config: self.class.action(action_name),
106
+ params: params,
107
+ graphql_context: graphql_request.context
108
+ ).call
109
+ end
93
110
  end
94
111
  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) }
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,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,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlRails
4
+ module Integrations
5
+ # lograge integration
6
+ #
7
+ # usage:
8
+ # add `GraphqlRails::Integrations::Lograge.enable` in your initializers
9
+ module Lograge
10
+ require 'lograge'
11
+
12
+ # lograge subscriber for graphql_rails controller events
13
+ class GraphqlActionControllerSubscriber < ::Lograge::LogSubscribers::Base
14
+ def process_action(event)
15
+ process_main_event(event)
16
+ end
17
+
18
+ private
19
+
20
+ def initial_data(payload)
21
+ {
22
+ controller: payload[:controller],
23
+ action: payload[:action]
24
+ }
25
+ end
26
+ end
27
+
28
+ def self.enable
29
+ return unless active?
30
+
31
+ GraphqlActionControllerSubscriber.attach_to :graphql_action_controller
32
+ end
33
+
34
+ def self.active?
35
+ !defined?(Rails) || Rails.configuration&.lograge&.enabled
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlRails
4
+ module Integrations
5
+ # sentry integration
6
+ module Sentry
7
+ require 'active_support/concern'
8
+
9
+ # controller extension which logs errors to sentry
10
+ module SentryLogger
11
+ extend ActiveSupport::Concern
12
+
13
+ included do
14
+ around_action :log_to_sentry
15
+
16
+ protected
17
+
18
+ def log_to_sentry
19
+ Raven.context.transaction.pop
20
+ Raven.context.transaction.push "#{self.class}##{action_name}"
21
+ yield
22
+ rescue Exception => error # rubocop:disable Lint/RescueException
23
+ Raven.capture_exception(error) unless error.is_a?(GraphQL::ExecutionError)
24
+ raise error
25
+ end
26
+ end
27
+ end
28
+
29
+ def self.enable
30
+ GraphqlRails::Controller.include(SentryLogger)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlRails
4
+ # allows to enable various integrations
5
+ module Integrations
6
+ def self.enable(*integrations)
7
+ @enabled_integrations ||= []
8
+
9
+ to_be_enabled_integrations = integrations.map(&:to_s) - @enabled_integrations
10
+
11
+ to_be_enabled_integrations.each do |integration|
12
+ require_relative "./integrations/#{integration}"
13
+ Integrations.const_get(integration.classify).enable
14
+ end
15
+
16
+ @enabled_integrations += to_be_enabled_integrations
17
+ end
18
+ end
19
+ end
@@ -17,13 +17,26 @@ module GraphqlRails
17
17
  @model_class = model_class
18
18
  end
19
19
 
20
- def attribute(attribute_name, type: nil, **attribute_options)
21
- attributes[attribute_name.to_s] = \
22
- Attributes::Attribute.new(
23
- attribute_name,
24
- type,
25
- attribute_options
26
- )
20
+ def initialize_copy(other)
21
+ super
22
+ @connection_type = nil
23
+ @graphql_type = nil
24
+ @input = other.instance_variable_get(:@input)&.transform_values(&:dup)
25
+ @attributes = other.instance_variable_get(:@attributes)&.transform_values(&:dup)
26
+ end
27
+
28
+ def attribute(attribute_name, **attribute_options)
29
+ key = attribute_name.to_s
30
+
31
+ attributes[key] ||= Attributes::Attribute.new(attribute_name)
32
+
33
+ attributes[key].tap do |attribute|
34
+ attribute_options.each do |method_name, args|
35
+ attribute.public_send(method_name, args)
36
+ end
37
+
38
+ yield(attribute) if block_given?
39
+ end
27
40
  end
28
41
 
29
42
  def input(input_name = nil)
@@ -35,7 +35,7 @@ module GraphqlRails
35
35
 
36
36
  def default_name
37
37
  @default_name ||= begin
38
- suffix = input_name_suffix ? input_name_suffix.to_s.tableize : ''
38
+ suffix = input_name_suffix ? input_name_suffix.to_s.camelize : ''
39
39
  "#{model_class.name.split('::').last}#{suffix}Input"
40
40
  end
41
41
  end
@@ -23,6 +23,13 @@ module GraphqlRails
23
23
 
24
24
  # static methods for GraphqlRails::Model
25
25
  module ClassMethods
26
+ def inherited(subclass)
27
+ super
28
+ subclass.instance_variable_set(:@graphql, graphql.dup)
29
+ subclass.graphql.instance_variable_set(:@model_class, self)
30
+ subclass.graphql.instance_variable_set(:@graphql_type, nil)
31
+ end
32
+
26
33
  def graphql
27
34
  @graphql ||= Model::Configuration.new(self)
28
35
  yield(@graphql) if block_given?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GraphqlRails
4
- VERSION = '0.7.0'
4
+ VERSION = '0.8.0'
5
5
  end