rails-graphql 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cdf2022ac5afdaa3bc0f3068625508c47d5f2df97325427f214f79030f72778b
4
- data.tar.gz: 0176fe930c8a991f98c2ad28afb2ebce462fc4a64ad9565fffd8fd29e5ca9019
3
+ metadata.gz: 8a4e4946f91fcba1ff47ce9ba25b56af87b2a5652f1d414b4d949b46d8e81bb5
4
+ data.tar.gz: dbd510340b18b079ace3237f5ffb9721b752af2795e34e8778c5687dc9d2af1f
5
5
  SHA512:
6
- metadata.gz: 0fc23bb1c616165efbdb46ee04697308245a63f6345418dfb5e972d99543c527abcd18cd926786bc9f8de444a39fda60791e1570c4f60b096a0dea501c7bfaeb
7
- data.tar.gz: f09ed8ccfb57024a3334589ce1f609efb29eef330f1af235a47d232b881953472e9b2c25190e3b6b3cefbc4927ae1fef841d171dc82d1e2156874b70c5ea3588
6
+ metadata.gz: df0dbeb5e6d27955c328d7f1c7051324ca47f71e279df2fa0e927cf0d9af0b150c33c209fa1fd3fc09701ce5e0df0137e88c8faff04bf602bc798dd2eda4ea71
7
+ data.tar.gz: 03f61e059ebea710f08d22452dc736d35efae12174b3a74aff67490fee80b8edf08124f2b17f43656f525b893a548b2c99ff2607dc5442059fa2ab6fef71ca4e
@@ -136,9 +136,7 @@ module Rails # :nodoc:
136
136
 
137
137
  if (source = xargs.delete(:source)).present?
138
138
  location = xargs.delete(:location) || source.try(:directive_location)
139
- event ||= GraphQL::Event.new(:attach, source, **xargs.reverse_merge(
140
- phase: :definition,
141
- ))
139
+ event ||= GraphQL::Event.new(:attach, source, phase: :definition, **xargs)
142
140
  end
143
141
 
144
142
  Array.wrap(list).each_with_object(Set.new) do |item, result|
@@ -15,7 +15,7 @@ module Rails # :nodoc:
15
15
 
16
16
  # Directives need to be contextualized by the given instance as +context+
17
17
  def self.set_context(item, context)
18
- lambda { |*args| item.call(*args, context: context) }
18
+ lambda { |*args, **xargs| item.call(*args, _callback_context: context, **xargs) }
19
19
  end
20
20
 
21
21
  def initialize(target, event_name, *args, **xargs, &block)
@@ -46,9 +46,12 @@ module Rails # :nodoc:
46
46
 
47
47
  # This does the whole checking and preparation in order to really execute
48
48
  # the callback method
49
- def call(event, context: nil)
49
+ def call(event, *args, _callback_context: nil, **xargs)
50
50
  return unless event.name === event_name && can_run?(event)
51
- block.is_a?(Symbol) ? call_symbol(event) : call_proc(event, context)
51
+
52
+ block.is_a?(Symbol) \
53
+ ? call_symbol(event, *args, **xargs) \
54
+ : call_proc(event, _callback_context, *args, **xargs)
52
55
  end
53
56
 
54
57
  # Get a described source location for the callback
@@ -68,38 +71,45 @@ module Rails # :nodoc:
68
71
 
69
72
  private
70
73
 
74
+ # Find the proper owner of the symbol based callback
75
+ def owner
76
+ @owner ||= target.all_owners.find do |item|
77
+ item.is_a?(Class) ? item.method_defined?(block) : item.respond_to?(block)
78
+ end || target
79
+ end
80
+
71
81
  # Using the filters, check if the current callback can be executed
72
82
  def can_run?(event)
73
83
  filters.all? { |key, options| event_filters[key][:block].call(options, event) }
74
84
  end
75
85
 
76
86
  # Call the callback block as a symbol
77
- def call_symbol(event)
78
- owner = target.try(:proxied_owner) || target.try(:owner) || target
87
+ def call_symbol(event, *args, **xargs)
79
88
  event.on_instance(owner) do |instance|
80
89
  block = instance.method(@block)
81
- args, xargs = collect_parameters(event, block)
90
+ args, xargs = collect_parameters(event, [args, xargs], block)
82
91
  block.call(*args, **xargs)
83
92
  end
84
93
  end
85
94
 
86
95
  # Call the callback block as a proc
87
- def call_proc(event, context = nil)
88
- args, xargs = collect_parameters(event)
96
+ def call_proc(event, context = nil, *args, **xargs)
97
+ args, xargs = collect_parameters(event, [args, xargs])
89
98
  (context || event).instance_exec(*args, **xargs, &block)
90
99
  end
91
100
 
92
101
  # Read the arguments needed for a block then collect them from the
93
102
  # event and return the execution args
94
- def collect_parameters(event, block = @block)
95
- args_source = event.send(:args_source) || event
96
- start_args = [@pre_args.deep_dup, @pre_xargs.deep_dup]
97
- return start_args unless inject_arguments?
103
+ def collect_parameters(event, send_args, block = @block)
104
+ args_source = event.send(:args_source)
105
+ send_args[0] += @pre_args.deep_dup
106
+ send_args[1].merge!(@pre_xargs.deep_dup)
107
+ return send_args unless inject_arguments?
98
108
 
99
109
  # TODO: Maybe we need to turn procs into lambdas so the optional
100
110
  # arguments doesn't suffer any kind of change
101
111
  idx = -1
102
- block.parameters.each_with_object(start_args) do |(type, name), result|
112
+ block.parameters.each_with_object(send_args) do |(type, name), result|
103
113
  case type
104
114
  when :opt, :req
105
115
  idx += 1
@@ -38,5 +38,9 @@ module Rails # :nodoc:
38
38
  # Error class related to when the captured output value is invalid due to
39
39
  # type checking
40
40
  InvalidValueError = Class.new(FieldError)
41
+
42
+ # Error class related to when a field is unauthorized and can not be used,
43
+ # similar to disabled fields
44
+ UnauthorizedFieldError = Class.new(FieldError)
41
45
  end
42
46
  end
@@ -108,7 +108,7 @@ module Rails # :nodoc:
108
108
 
109
109
  # Call a given block and send the event as reference
110
110
  def trigger(block)
111
- catchable(:item) { @last_result = block.call(self) }
111
+ @last_result = catchable(:item) { block.call(self) }
112
112
  end
113
113
 
114
114
  # Stop the execution of an event using a given +layer+. The default is to
@@ -127,6 +127,10 @@ module Rails # :nodoc:
127
127
 
128
128
  alias call_super call_next
129
129
 
130
+ protected
131
+
132
+ alias args_source itself
133
+
130
134
  private
131
135
 
132
136
  # Add the layer, exec the block and remove the layer
@@ -136,6 +140,17 @@ module Rails # :nodoc:
136
140
  ensure
137
141
  @layers.pop
138
142
  end
143
+
144
+ # Check for data based readers
145
+ def respond_to_missing?(method_name, include_private = false)
146
+ data.key?(method_name) || super
147
+ end
148
+
149
+ # If the +method_name+ matches any key entry of the provided data, just
150
+ # return the value stored there
151
+ def method_missing(method_name, *)
152
+ data.key?(method_name) ? data[method_name] : super
153
+ end
139
154
  end
140
155
  end
141
156
  end
@@ -38,20 +38,20 @@ module Rails # :nodoc:
38
38
 
39
39
  autoload :ScopedConfig
40
40
 
41
+ autoload :AuthorizedField
42
+ autoload :ProxiedField
41
43
  autoload :ResolvedField
42
44
  autoload :TypedField
43
- autoload :ProxiedField
44
45
 
45
46
  autoload :InputField
46
47
  autoload :OutputField
47
48
  autoload :MutationField
48
49
 
49
- delegate :input_type?, :output_type?, :leaf_type?, :proxy?, :mutation?, to: :class
50
+ attr_reader :name, :gql_name, :owner
50
51
 
52
+ delegate :input_type?, :output_type?, :leaf_type?, :proxy?, :mutation?, to: :class
51
53
  delegate :namespaces, to: :owner
52
54
 
53
- attr_reader :name, :gql_name, :owner
54
-
55
55
  class << self
56
56
  # A small shared helper method that allows field information to be
57
57
  # proxied
@@ -145,6 +145,11 @@ module Rails # :nodoc:
145
145
  (other.nullable? == nullable? || other.nullable? && !nullable?)
146
146
  end
147
147
 
148
+ # Return the owner as the single item of the list
149
+ def all_owners
150
+ [owner]
151
+ end
152
+
148
153
  # Checks if the argument can be null
149
154
  def null?
150
155
  !!@null
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails # :nodoc:
4
+ module GraphQL # :nodoc:
5
+ # This provides ways for fields to be authorized, giving a logical level for
6
+ # enabling or disabling access to a field. It has a similar structure to
7
+ # events, but has a different hierarchy of resolution
8
+ module Field::AuthorizedField
9
+ # Just add the callbacks setup to the field
10
+ def self.included(other)
11
+ other.event_types(:authorize, append: true)
12
+ end
13
+
14
+ # Add either settings for authorization or a block to be executed. It
15
+ # returns +self+ for chain purposes
16
+ def authorize(*args, **xargs, &block)
17
+ @authorizer = [args, xargs, block]
18
+ self
19
+ end
20
+
21
+ # Return the settings for the authorize process
22
+ def authorizer
23
+ @authorizer if authorizable?
24
+ end
25
+
26
+ # Checks if the field should go through an authorization process
27
+ def authorizable?
28
+ defined?(@authorizer)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ #
@@ -16,9 +16,10 @@ module Rails # :nodoc:
16
16
  end
17
17
 
18
18
  # Add a block or a callable method that is executed before the resolver
19
- # but after all the before resolve
19
+ # but after all the before resolve. It returns +self+ for chain purposes
20
20
  def perform(*args, **xargs, &block)
21
21
  @performer = Callback.new(self, :perform, *args, **xargs, &block)
22
+ self
22
23
  end
23
24
 
24
25
  # Get the performer that can be already defined or used through the
@@ -10,6 +10,10 @@ module Rails # :nodoc:
10
10
  class Field::OutputField < Field
11
11
  include Helpers::WithArguments
12
12
  include Helpers::WithValidator
13
+ include Helpers::WithEvents
14
+ include Helpers::WithCallbacks
15
+
16
+ include Field::AuthorizedField
13
17
  include Field::ResolvedField
14
18
  include Field::TypedField
15
19
 
@@ -68,6 +68,11 @@ module Rails # :nodoc:
68
68
  field.owner
69
69
  end
70
70
 
71
+ # Override this to include proxied owners
72
+ def all_owners
73
+ super + proxied_owner.all_owners
74
+ end
75
+
71
76
  # Return the proxied field
72
77
  def proxied_field
73
78
  @field
@@ -29,16 +29,16 @@ module Rails # :nodoc:
29
29
 
30
30
  # Just add the callbacks setup to the field
31
31
  def self.included(other)
32
- other.include(Helpers::WithEvents)
33
- other.include(Helpers::WithCallbacks)
34
- other.event_types(:prepare, :finalize, expose: true)
32
+ other.event_types(:prepare, :finalize, append: true, expose: true)
35
33
  other.alias_method(:before_resolve, :prepare)
36
34
  other.alias_method(:after_resolve, :finalize)
37
35
  end
38
36
 
39
- # Add a block that is performed while resolving a value of a field
37
+ # Add a block that is performed while resolving a value of a field. It
38
+ # returns +self+ for chain purposes
40
39
  def resolve(*args, **xargs, &block)
41
40
  @resolver = Callback.new(self, :resolve, *args, **xargs, &block)
41
+ self
42
42
  end
43
43
 
44
44
  # Get the resolver that can be already defined or used through the
@@ -6,7 +6,7 @@ module Rails # :nodoc:
6
6
  # instance of this class works as proxy for changes to the actual field.
7
7
  Field::ScopedConfig = Struct.new(:field, :receiver) do
8
8
  delegate :argument, :ref_argument, :id_argument, :use, :internal?, :disabled?,
9
- :enabled?, :disable!, :enable!, to: :field
9
+ :enabled?, :disable!, :enable!, :authorize, to: :field
10
10
 
11
11
  delegate_missing_to :receiver
12
12
 
@@ -9,7 +9,7 @@ module Rails # :nodoc:
9
9
  # symbolic methods
10
10
  module WithCallbacks
11
11
  DEFAULT_EVENT_TYPES = %i[query mutation subscription request attach
12
- organized prepared finalize].freeze
12
+ authorize organized prepared finalize].freeze
13
13
 
14
14
  def self.extended(other)
15
15
  other.extend(WithCallbacks::Setup)
@@ -27,20 +27,20 @@ module Rails # :nodoc:
27
27
  # Set or get the list of possible event types when attaching events
28
28
  def event_types(*list, append: false, expose: false)
29
29
  return (defined?(@event_types) && @event_types.presence) ||
30
- superclass.try(:event_types) if list.blank?
30
+ superclass.try(:event_types) || [] if list.blank?
31
31
 
32
- list = event_types if append
33
- list += list.flatten.compact.map(&:to_sym)
34
- @event_types = list.uniq.freeze
35
- expose_events! if expose
32
+ new_list = append ? event_types : []
33
+ new_list += list.flatten.compact.map(&:to_sym)
34
+ @event_types = new_list.uniq.freeze
35
+ expose_events!(*list) if expose
36
36
  @event_types
37
37
  end
38
38
 
39
39
  protected
40
40
 
41
41
  # Auxiliar method that creates easy-accessible callback assignment
42
- def expose_events!
43
- event_types.each do |event_name|
42
+ def expose_events!(*list)
43
+ list.each do |event_name|
44
44
  next if method_defined?(event_name)
45
45
  define_method(event_name) do |*args, **xargs, &block|
46
46
  on(event_name, *args, **xargs, &block)
@@ -51,7 +51,7 @@ module Rails # :nodoc:
51
51
 
52
52
  # Mostly for correct inheritance on instances
53
53
  def all_events
54
- current = (@events || {})
54
+ current = defined?(@events) ? @events : {}
55
55
  return current unless defined? super
56
56
  Helpers.merge_hash_array(current, super)
57
57
  end
@@ -6,9 +6,16 @@ module Rails # :nodoc:
6
6
  # Helper module that allows other objects to hold an +assigned_to+ object
7
7
  module WithOwner
8
8
  def self.included(other)
9
+ other.extend(WithOwner::ClassMethods)
9
10
  other.class_attribute(:owner, instance_writer: false)
10
11
  end
11
12
 
13
+ module ClassMethods # :nodoc: all
14
+ def method_defined?(method_name)
15
+ super || owner&.method_defined?(method_name)
16
+ end
17
+ end
18
+
12
19
  private
13
20
 
14
21
  def respond_to_missing?(*args) # :nodoc:
@@ -24,8 +31,9 @@ module Rails # :nodoc:
24
31
 
25
32
  # Since owners are classes, this checks for the instance methods of
26
33
  # it, since this is a instance method
27
- def owner_respond_to?(method_name, include_private = false)
28
- (include_private ? %i[public protected private] : %i[public]).any? do |type|
34
+ def owner_respond_to?(method_name, with_private = false)
35
+ return true if !owner.is_a?(Class) && owner.respond_to?(method_name, with_private)
36
+ (with_private ? %i[public protected private] : %i[public]).any? do |type|
29
37
  owner.send("#{type}_instance_methods").include?(method_name)
30
38
  end unless owner.nil?
31
39
  end
@@ -189,7 +189,11 @@ module Rails # :nodoc:
189
189
  SCHEMA_FIELD_TYPES.each do |kind, type_name|
190
190
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
191
191
  def #{kind}_field?(name)
192
- field?(:#{kind}, name)
192
+ has_field?(:#{kind}, name)
193
+ end
194
+
195
+ def #{kind}_field(name)
196
+ find_field(:#{kind}, name)
193
197
  end
194
198
 
195
199
  def #{kind}_fields(&block)
@@ -20,6 +20,7 @@ module Rails # :nodoc:
20
20
 
21
21
  eager_autoload do
22
22
  autoload_under :steps do
23
+ autoload :Authorizable
23
24
  autoload :Organizable
24
25
  autoload :Prepareable
25
26
  autoload :Resolveable
@@ -42,8 +43,6 @@ module Rails # :nodoc:
42
43
  attr_reader :schema, :visitor, :operations, :fragments, :errors,
43
44
  :args, :response, :strategy, :stack
44
45
 
45
- delegate :all_listeners, to: :schema
46
-
47
46
  class << self
48
47
  # Shortcut for initialize, set context, and execute
49
48
  def execute(*args, schema: nil, namespace: :base, context: {}, **xargs)
@@ -79,6 +78,11 @@ module Rails # :nodoc:
79
78
  ensure_schema!
80
79
  end
81
80
 
81
+ # Cache all the schema listeners for this current request
82
+ def all_listeners
83
+ @all_listeners ||= schema.all_listeners
84
+ end
85
+
82
86
  # Cache all the schema events for this current request
83
87
  def all_events
84
88
  @all_events ||= schema.all_events
@@ -127,7 +131,7 @@ module Rails # :nodoc:
127
131
  # Add the given +exception+ to the errors using the +node+ location
128
132
  def exception_to_error(exception, node, **xargs)
129
133
  xargs[:exception] = exception.class.name
130
- report_node_error(exception.message, node, **xargs)
134
+ report_node_error(xargs.delete(:message) || exception.message, node, **xargs)
131
135
  end
132
136
 
133
137
  # A little helper to report an error on a given node
@@ -8,6 +8,7 @@ module Rails # :nodoc:
8
8
  # This class holds information about a given field that should be
9
9
  # collected from the source of where it was requested.
10
10
  class Component::Field < Component
11
+ include Authorizable
11
12
  include ValueWriters
12
13
  include SelectionSet
13
14
  include Directives
@@ -40,13 +41,16 @@ module Rails # :nodoc:
40
41
  # Override that considers the requested field directives and also the
41
42
  # definition field events, both from itself and its directives events
42
43
  def all_listeners
43
- field.all_listeners + super
44
+ (request.cache(:listeners)[field] ||= field.all_listeners) + super
44
45
  end
45
46
 
46
47
  # Override that considers the requested field directives and also the
47
48
  # definition field events, both from itself and its directives events
48
49
  def all_events
49
- @all_events ||= Helpers.merge_hash_array(field.all_events, super)
50
+ @all_events ||= Helpers.merge_hash_array(
51
+ (request.cache(:events)[field] ||= field.all_events),
52
+ super,
53
+ )
50
54
  end
51
55
 
52
56
  # Get and cache all the arguments for the field
@@ -129,6 +133,7 @@ module Rails # :nodoc:
129
133
  def organize_then(&block)
130
134
  super(block) do
131
135
  check_assignment!
136
+ check_authorization!
132
137
 
133
138
  parse_arguments
134
139
  parse_directives
@@ -183,10 +188,10 @@ module Rails # :nodoc:
183
188
  def trigger_event(event_name, **xargs)
184
189
  return super if !defined?(@current_object) || @current_object.nil?
185
190
 
186
- listeners = request.cache(:dynamic_listeners)[field] ||= field.all_listeners
191
+ listeners = request.cache(:listeners)[field] ||= field.all_listeners
187
192
  return super unless listeners.include?(event_name)
188
193
 
189
- callbacks = request.cache(:dynamic_events)[field] ||= field.all_events
194
+ callbacks = request.cache(:events)[field] ||= field.all_events
190
195
  old_events, @all_events = @all_events, callbacks
191
196
  super
192
197
  ensure
@@ -70,13 +70,13 @@ module Rails # :nodoc:
70
70
  args_source.try(:[], name.to_sym)
71
71
  end
72
72
 
73
+ alias arg argument
74
+
73
75
  # A combined helper for +instance_for+ and +set_on+
74
- def on_instance(klass, &block)
75
- set_on(klass.is_a?(Class) ? instance_for(klass) : klass, &block)
76
+ def on_instance(object, &block)
77
+ set_on(object.is_a?(Class) ? instance_for(object) : object, &block)
76
78
  end
77
79
 
78
- alias arg argument
79
-
80
80
  protected
81
81
 
82
82
  # When performing an event under a field object, the keyed-based
@@ -6,12 +6,13 @@ module Rails # :nodoc:
6
6
  # Helper module to collect the directives from fragments, operations, and
7
7
  # fields.
8
8
  module Directives
9
- # Get the list of listeners from all directives
9
+ # Get the list of listeners from directives set during the request only
10
10
  def all_listeners
11
11
  directives.map(&:all_listeners).reduce(:+) || Set.new
12
12
  end
13
13
 
14
- # Get the list of events from all directives and caches it by request
14
+ # Get the list of events from directives set during the request only and
15
+ # then caches it by request
15
16
  def all_events
16
17
  @all_events ||= directives.map(&:all_events).inject({}) do |lhash, rhash|
17
18
  Helpers.merge_hash_array(lhash, rhash)
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails # :nodoc:
4
+ module GraphQL # :nodoc:
5
+ class Request # :nodoc:
6
+ # Helper methods for the authorize step of a request
7
+ module Authorizable
8
+ # Event used to perform an authorization step
9
+ class Event < GraphQL::Event
10
+ # Similar to trigger for object, but with an extra extension for
11
+ # instance methods defined on the given object
12
+ def authorize_using(object, send_args, events = nil)
13
+ cache = data[:request].cache(name)[object] ||= []
14
+ return false if cache.present? && cache.none?
15
+ args, xargs = send_args
16
+
17
+ # Authorize through instance method
18
+ using_object = cache[0] ||= authorize_on_object(object)
19
+ set_on(using_object) do |instance|
20
+ instance.public_send("#{name}!", self, *args, **xargs)
21
+ end if using_object
22
+
23
+ # Authorize through events
24
+ using_events = cache[1] ||= (events || object.all_events[name]).presence
25
+ using_events&.each { |block| block.call(self, *args, **xargs) }
26
+
27
+ # Does any authorize process ran
28
+ cache.any?
29
+ end
30
+
31
+ # Simply unauthorize the operation
32
+ def unauthorize!(*, message: nil, **)
33
+ raise UnauthorizedFieldError, message || <<~MSG.squish
34
+ Unauthorized access to "#{field.gql_name}" field.
35
+ MSG
36
+ end
37
+
38
+ # Simply authorize the operation
39
+ def authorize!(*)
40
+ throw :authorized
41
+ end
42
+
43
+ private
44
+
45
+ # Check if it should run call an +authorize!+ method on the given
46
+ # +object+. Classes are turn into instance through strategy
47
+ def authorize_on_object(object)
48
+ as_class = object.is_a?(Class)
49
+ checker = as_class ? :method_defined? : :respond_to?
50
+
51
+ return false unless object.public_send(checker, :authorize!)
52
+ as_class ? data[:request].strategy.instance_for(object) : object
53
+ end
54
+ end
55
+
56
+ # Check if the field is correctly authorized to be executed
57
+ def check_authorization!
58
+ return unless field.authorizable?
59
+ *args, block = field.authorizer
60
+
61
+ catch(:authorized) do
62
+ event = authorization_event
63
+ schema_events = request.all_events[:authorize]
64
+ executed = event.authorize_using(schema, args, schema_events)
65
+
66
+ element = field
67
+ while element && element != schema
68
+ executed = event.authorize_using(element, args) || executed
69
+ element = element.try(:owner)
70
+ end
71
+
72
+ if block.present?
73
+ block.call(event, *args[0], **args[1])
74
+ executed = true
75
+ end
76
+
77
+ event.unauthorize!(message: <<~MSG.squish) unless executed
78
+ Authorization required but unable to be executed
79
+ MSG
80
+ end
81
+ rescue UnauthorizedFieldError => error
82
+ request.rescue_with_handler(error)
83
+ request.exception_to_error(error, @node)
84
+ invalidate!
85
+ end
86
+
87
+ private
88
+
89
+ # Build and store the authorization event
90
+ def authorization_event
91
+ Event.new(:authorize, self,
92
+ request: request,
93
+ schema: schema,
94
+ field: field,
95
+ memo: memo,
96
+ )
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -37,6 +37,7 @@ module Rails # :nodoc:
37
37
 
38
38
  def initialize(request)
39
39
  @request = request
40
+ @objects_pool = {}
40
41
  collect_request_listeners
41
42
  end
42
43
 
@@ -205,7 +206,6 @@ module Rails # :nodoc:
205
206
  # it can load data in a pretty smart way
206
207
  def collect_data
207
208
  @data_pool = {}
208
- @objects_pool = {}
209
209
  @context = request.build(Request::Context)
210
210
 
211
211
  # TODO: Create an orchestrator to allow cross query loading
@@ -150,13 +150,13 @@ module Rails # :nodoc:
150
150
  end
151
151
 
152
152
  # Prepare to load multiple records from the underlying table
153
- def load_records
154
- inject_scopes(model.all, :relation)
153
+ def load_records(scope = model.default_scoped)
154
+ inject_scopes(scope, :relation)
155
155
  end
156
156
 
157
157
  # Prepare to load a single record from the underlying table
158
- def load_record
159
- load_records.find(event.argument(primary_key))
158
+ def load_record(scope = model.default_scoped)
159
+ scope.find(event.argument(primary_key))
160
160
  end
161
161
 
162
162
  # Get the chain result and preload the records with thre resulting scope
@@ -166,7 +166,7 @@ module Rails # :nodoc:
166
166
 
167
167
  # Collect a scope for filters applied to a given association
168
168
  def build_association_scope(association)
169
- scope = model._reflect_on_association(association).klass.unscoped
169
+ scope = model._reflect_on_association(association).klass.default_scoped
170
170
 
171
171
  # Apply proxied injected scopes
172
172
  proxied = event.field.try(:proxied_owner)
@@ -45,7 +45,7 @@ module Rails # :nodoc:
45
45
  # Add a new scoped param to the list
46
46
  def scoped_argument(param, type = :string, proc_method = nil, **settings, &block)
47
47
  block = proc_method if proc_method.present? && block.nil?
48
- argument = Argument.new(param, type, **settings.merge(owner: self), &block)
48
+ argument = Argument.new(param, type, **settings, owner: self, &block)
49
49
  (@scoped_arguments ||= {})[argument.name] = argument
50
50
  end
51
51
 
@@ -223,11 +223,11 @@ module Rails # :nodoc:
223
223
  namespaces += [:base] unless namespaces.include?(:base) || exclusive
224
224
 
225
225
  iterated = []
226
- enumerator = Enumerator::Lazy.new(namespaces.uniq) do |yielder, *values|
227
- next unless @index.key?(values.last)
226
+ enumerator = Enumerator::Lazy.new(namespaces.uniq) do |yielder, item|
227
+ next unless @index.key?(item)
228
228
 
229
229
  # Only iterate over string based types
230
- @index[values.last][base_class]&.each do |_key, value|
230
+ @index[item][base_class]&.each do |_key, value|
231
231
  next if iterated.include?(value = value.call) || value.blank?
232
232
  iterated << value
233
233
  yielder << value
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rails # :nodoc:
4
4
  module GraphQL # :nodoc:
5
- VERSION = '0.1.3'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
@@ -0,0 +1,112 @@
1
+ require 'integration/config'
2
+
3
+ class Integration_Memory_AuthorizationTest < GraphQL::IntegrationTestCase
4
+ load_schema 'authorization'
5
+
6
+ SCHEMA = ::AuthorizationSchema
7
+ EXCEPTION_NAME = 'Rails::GraphQL::UnauthorizedFieldError'
8
+ EXCEPTION_PATH = ['errors', 0, 'extensions', 'exception']
9
+
10
+ SAMPLE1 = { data: { sample1: 'Ok 1' } }
11
+ SAMPLE2 = { data: { sample2: 'Ok 2' } }
12
+
13
+ def test_field_event_types
14
+ assert_includes(SCHEMA.query_field(:sample1).event_types, :authorize)
15
+ end
16
+
17
+ def test_without_authorization
18
+ assert_result(SAMPLE1, '{ sample1 }')
19
+ end
20
+
21
+ def test_with_simple_authorization
22
+ assert_exception('{ sample2 }')
23
+ assert_result({ sample1: 'Ok 1', sample2: nil }, '{ sample1 sample2 }', dig: 'data')
24
+
25
+ SCHEMA.stub_imethod(:authorize!, &:authorize!).call do
26
+ assert_result(SAMPLE2, '{ sample2 }')
27
+ end
28
+
29
+ SCHEMA.stub_imethod(:authorize!, &:unauthorize!).call do
30
+ assert_exception('{ sample2 }')
31
+ end
32
+ end
33
+
34
+ def test_with_block_authorization
35
+ field = SCHEMA.query_field(:sample1)
36
+
37
+ field.stub_ivar(:@authorizer, [[], {}, method(:executed!)]) do
38
+ assert_executed { assert_result(SAMPLE1, '{ sample1 }') }
39
+ end
40
+
41
+ block = ->(ev) { executed! && ev.unauthorize! }
42
+ field.stub_ivar(:@authorizer, [[], {}, block]) do
43
+ assert_executed { assert_exception('{ sample1 }') }
44
+ end
45
+ end
46
+
47
+ def test_with_field_event_authorization
48
+ field = SCHEMA.query_field(:sample2)
49
+
50
+ field.stub_ivar(:@events, { authorize: [method(:executed!)] }) do
51
+ assert_executed { assert_result(SAMPLE2, '{ sample2 }') }
52
+
53
+ assert_executed do
54
+ field.on(:authorize) { |event| event.unauthorize! }
55
+ assert_exception('{ sample2 }')
56
+ end
57
+ end
58
+ end
59
+
60
+ def test_with_directive
61
+ field = SCHEMA.query_field(:sample2)
62
+
63
+ auth_directive = unmapped_class(Rails::GraphQL::Directive)
64
+ auth_directive.on(:authorize, &method(:executed!))
65
+
66
+ auth_directive.stub_ivar(:@gql_name, 'AuthDirective') do
67
+ field.stub_ivar(:@directives, [auth_directive.new]) do
68
+ assert_executed { assert_result(SAMPLE2, '{ sample2 }') }
69
+ end
70
+
71
+ SCHEMA.stub_ivar(:@directives, [auth_directive.new]) do
72
+ assert_executed { assert_result(SAMPLE2, '{ sample2 }') }
73
+ end
74
+ end
75
+ end
76
+
77
+ def test_authorize_bypass
78
+ field = SCHEMA.query_field(:sample2)
79
+ auth_block = ->(e, event) { e.call && event.authorize! }.curry(2)[method(:executed!)]
80
+ unauth_block = ->(e, event) { e.call && event.unauthorize! }.curry(2)[method(:executed!)]
81
+
82
+ field.stub_ivar(:@events, { authorize: [unauth_block] }) do
83
+ SCHEMA.stub_imethod(:authorize!, &auth_block).call do
84
+ assert_executed { assert_result(SAMPLE2, '{ sample2 }') }
85
+ end
86
+ end
87
+
88
+ field.stub_ivar(:@events, { authorize: [auth_block] }) do
89
+ SCHEMA.stub_imethod(:authorize!, &unauth_block).call do
90
+ assert_executed { assert_exception('{ sample2 }') }
91
+ end
92
+ end
93
+ end
94
+
95
+ protected
96
+
97
+ def executed!(*)
98
+ @executed += 1
99
+ end
100
+
101
+ def assert_executed(times = 1)
102
+ @executed = 0
103
+ yield
104
+ assert_equal(times, @executed)
105
+ ensure
106
+ remove_instance_variable(:@executed)
107
+ end
108
+
109
+ def assert_exception(query, *args, **xargs)
110
+ assert_result(EXCEPTION_NAME, query, *args, dig: EXCEPTION_PATH, **xargs)
111
+ end
112
+ end
@@ -0,0 +1,12 @@
1
+ class AuthorizationSchema < GraphQL::Schema
2
+ namespace :authorization
3
+
4
+ configure do |config|
5
+ config.enable_string_collector = false
6
+ end
7
+
8
+ query_fields do
9
+ field(:sample1, :string).resolve { 'Ok 1' }
10
+ field(:sample2, :string).authorize.resolve { 'Ok 2' }
11
+ end
12
+ end
@@ -28,6 +28,20 @@ class Object < BasicObject
28
28
  const_set(name, old_value) if defined? old_value
29
29
  end
30
30
 
31
+ def stub_imethod(name, &block)
32
+ lambda do |&run_block|
33
+ alias_method(:"_old_#{name}", name) if (reset_old = method_defined?(name))
34
+ define_method(name, &block)
35
+ run_block.call
36
+ ensure
37
+ undef_method(name)
38
+ if reset_old
39
+ alias_method(name, :"_old_#{name}")
40
+ undef_method(:"_old_#{name}")
41
+ end
42
+ end
43
+ end
44
+
31
45
  def get_reset_ivar(name, *extra, &block)
32
46
  instance_variable_set(name, extra.first) if extra.any?
33
47
  instance_exec(&block)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carlos Silva
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-22 00:00:00.000000000 Z
11
+ date: 2021-01-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -307,6 +307,7 @@ files:
307
307
  - lib/rails/graphql/errors.rb
308
308
  - lib/rails/graphql/event.rb
309
309
  - lib/rails/graphql/field.rb
310
+ - lib/rails/graphql/field/authorized_field.rb
310
311
  - lib/rails/graphql/field/input_field.rb
311
312
  - lib/rails/graphql/field/mutation_field.rb
312
313
  - lib/rails/graphql/field/output_field.rb
@@ -355,6 +356,7 @@ files:
355
356
  - lib/rails/graphql/request/helpers/directives.rb
356
357
  - lib/rails/graphql/request/helpers/selection_set.rb
357
358
  - lib/rails/graphql/request/helpers/value_writers.rb
359
+ - lib/rails/graphql/request/steps/authorizable.rb
358
360
  - lib/rails/graphql/request/steps/organizable.rb
359
361
  - lib/rails/graphql/request/steps/prepareable.rb
360
362
  - lib/rails/graphql/request/steps/resolveable.rb
@@ -427,10 +429,12 @@ files:
427
429
  - test/graphql/type_map_test.rb
428
430
  - test/graphql/type_test.rb
429
431
  - test/graphql_test.rb
432
+ - test/integration/authorization/authorization_test.rb
430
433
  - test/integration/config.rb
431
434
  - test/integration/memory/star_wars_introspection_test.rb
432
435
  - test/integration/memory/star_wars_query_test.rb
433
436
  - test/integration/memory/star_wars_validation_test.rb
437
+ - test/integration/schemas/authorization.rb
434
438
  - test/integration/schemas/memory.rb
435
439
  - test/integration/schemas/sqlite.rb
436
440
  - test/integration/sqlite/star_wars_introspection_test.rb
@@ -503,7 +507,9 @@ test_files:
503
507
  - test/integration/memory/star_wars_validation_test.rb
504
508
  - test/integration/schemas/memory.rb
505
509
  - test/integration/schemas/sqlite.rb
510
+ - test/integration/schemas/authorization.rb
506
511
  - test/integration/sqlite/star_wars_introspection_test.rb
507
512
  - test/integration/sqlite/star_wars_mutation_test.rb
508
513
  - test/integration/sqlite/star_wars_query_test.rb
514
+ - test/integration/authorization/authorization_test.rb
509
515
  - test/test_ext.rb