graphql_rails 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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