graphiti 1.2.16 → 1.3.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +96 -0
  3. data/.standard.yml +4 -4
  4. data/Appraisals +23 -17
  5. data/CHANGELOG.md +7 -1
  6. data/Guardfile +5 -5
  7. data/deprecated_generators/graphiti/generator_mixin.rb +1 -0
  8. data/deprecated_generators/graphiti/resource_generator.rb +1 -1
  9. data/gemfiles/{rails_5.gemfile → rails_5_2.gemfile} +2 -2
  10. data/gemfiles/{rails_5_graphiti_rails.gemfile → rails_5_2_graphiti_rails.gemfile} +3 -4
  11. data/gemfiles/rails_6.gemfile +1 -1
  12. data/gemfiles/rails_6_graphiti_rails.gemfile +2 -3
  13. data/gemfiles/{rails_4.gemfile → rails_7.gemfile} +2 -2
  14. data/gemfiles/rails_7_graphiti_rails.gemfile +19 -0
  15. data/graphiti.gemspec +16 -16
  16. data/lib/graphiti/adapters/abstract.rb +20 -5
  17. data/lib/graphiti/adapters/active_record/belongs_to_sideload.rb +1 -1
  18. data/lib/graphiti/adapters/active_record/has_many_sideload.rb +1 -1
  19. data/lib/graphiti/adapters/active_record/has_one_sideload.rb +1 -1
  20. data/lib/graphiti/adapters/active_record/{inferrence.rb → inference.rb} +2 -2
  21. data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +19 -0
  22. data/lib/graphiti/adapters/active_record.rb +119 -74
  23. data/lib/graphiti/adapters/graphiti_api.rb +1 -1
  24. data/lib/graphiti/adapters/null.rb +1 -1
  25. data/lib/graphiti/adapters/persistence/associations.rb +78 -0
  26. data/lib/graphiti/configuration.rb +3 -1
  27. data/lib/graphiti/debugger.rb +12 -8
  28. data/lib/graphiti/delegates/pagination.rb +47 -13
  29. data/lib/graphiti/deserializer.rb +3 -3
  30. data/lib/graphiti/errors.rb +109 -15
  31. data/lib/graphiti/extensions/extra_attribute.rb +4 -4
  32. data/lib/graphiti/extensions/temp_id.rb +1 -1
  33. data/lib/graphiti/filter_operators.rb +0 -1
  34. data/lib/graphiti/hash_renderer.rb +198 -21
  35. data/lib/graphiti/query.rb +105 -73
  36. data/lib/graphiti/railtie.rb +5 -5
  37. data/lib/graphiti/renderer.rb +19 -1
  38. data/lib/graphiti/request_validator.rb +10 -10
  39. data/lib/graphiti/request_validators/update_validator.rb +4 -5
  40. data/lib/graphiti/request_validators/validator.rb +38 -24
  41. data/lib/graphiti/resource/configuration.rb +35 -7
  42. data/lib/graphiti/resource/dsl.rb +34 -8
  43. data/lib/graphiti/resource/interface.rb +13 -3
  44. data/lib/graphiti/resource/links.rb +3 -3
  45. data/lib/graphiti/resource/persistence.rb +2 -1
  46. data/lib/graphiti/resource/polymorphism.rb +8 -2
  47. data/lib/graphiti/resource/remote.rb +2 -2
  48. data/lib/graphiti/resource/sideloading.rb +4 -4
  49. data/lib/graphiti/resource.rb +12 -1
  50. data/lib/graphiti/resource_proxy.rb +23 -3
  51. data/lib/graphiti/runner.rb +5 -5
  52. data/lib/graphiti/schema.rb +36 -11
  53. data/lib/graphiti/schema_diff.rb +44 -4
  54. data/lib/graphiti/scope.rb +8 -10
  55. data/lib/graphiti/scoping/base.rb +3 -3
  56. data/lib/graphiti/scoping/filter.rb +36 -15
  57. data/lib/graphiti/scoping/filter_group_validator.rb +78 -0
  58. data/lib/graphiti/scoping/paginate.rb +47 -3
  59. data/lib/graphiti/scoping/sort.rb +5 -7
  60. data/lib/graphiti/serializer.rb +49 -7
  61. data/lib/graphiti/sideload/belongs_to.rb +1 -1
  62. data/lib/graphiti/sideload/has_many.rb +19 -1
  63. data/lib/graphiti/sideload/many_to_many.rb +11 -4
  64. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +3 -4
  65. data/lib/graphiti/sideload.rb +47 -23
  66. data/lib/graphiti/stats/dsl.rb +0 -1
  67. data/lib/graphiti/stats/payload.rb +12 -9
  68. data/lib/graphiti/types.rb +15 -15
  69. data/lib/graphiti/util/attribute_check.rb +1 -1
  70. data/lib/graphiti/util/class.rb +6 -0
  71. data/lib/graphiti/util/link.rb +10 -2
  72. data/lib/graphiti/util/persistence.rb +21 -78
  73. data/lib/graphiti/util/relationship_payload.rb +4 -4
  74. data/lib/graphiti/util/remote_params.rb +9 -4
  75. data/lib/graphiti/util/remote_serializer.rb +1 -0
  76. data/lib/graphiti/util/serializer_attributes.rb +41 -11
  77. data/lib/graphiti/util/simple_errors.rb +4 -4
  78. data/lib/graphiti/util/transaction_hooks_recorder.rb +1 -1
  79. data/lib/graphiti/version.rb +1 -1
  80. data/lib/graphiti.rb +6 -3
  81. metadata +46 -37
  82. data/.travis.yml +0 -59
@@ -3,10 +3,10 @@ module Graphiti
3
3
  attr_accessor :object, :unpaginated_object
4
4
  attr_reader :pagination
5
5
  def initialize(object, resource, query, opts = {})
6
- @object = object
7
- @resource = resource
8
- @query = query
9
- @opts = opts
6
+ @object = object
7
+ @resource = resource
8
+ @query = query
9
+ @opts = opts
10
10
 
11
11
  @object = @resource.around_scoping(@object, @query.hash) { |scope|
12
12
  apply_scoping(scope, opts)
@@ -45,9 +45,7 @@ module Graphiti
45
45
  resolve_sideload = -> {
46
46
  Graphiti.context = graphiti_context
47
47
  sideload.resolve(results, q, parent_resource)
48
- if concurrent && defined?(ActiveRecord)
49
- ActiveRecord::Base.clear_active_connections!
50
- end
48
+ @resource.adapter.close if concurrent
51
49
  }
52
50
  if concurrent
53
51
  promises << Concurrent::Promise.execute(&resolve_sideload)
@@ -75,7 +73,7 @@ module Graphiti
75
73
  resource: @resource,
76
74
  params: @opts[:params],
77
75
  sideload: @opts[:sideload],
78
- parent: @opts[:parent],
76
+ parent: @opts[:parent]
79
77
  # Set once data is resolved within block
80
78
  # results: ...
81
79
  }
@@ -87,8 +85,8 @@ module Graphiti
87
85
  # Used to ensure the resource's serializer is used
88
86
  # Not one derived through the usual jsonapi-rb logic
89
87
  def assign_serializer(records)
90
- records.each do |r|
91
- @resource.decorate_record(r)
88
+ records.each_with_index do |r, index|
89
+ @resource.decorate_record(r, index)
92
90
  end
93
91
  end
94
92
 
@@ -25,9 +25,9 @@ module Graphiti
25
25
  # @param [Hash] opts configuration options used by subclasses
26
26
  def initialize(resource, query_hash, scope, opts = {})
27
27
  @query_hash = query_hash
28
- @resource = resource
29
- @scope = scope
30
- @opts = opts
28
+ @resource = resource
29
+ @scope = scope
30
+ @opts = opts
31
31
  end
32
32
 
33
33
  # Apply this scoping criteria.
@@ -3,6 +3,13 @@ module Graphiti
3
3
  include Scoping::Filterable
4
4
 
5
5
  def apply
6
+ unless @opts[:bypass_required_filters]
7
+ Graphiti::Scoping::FilterGroupValidator.new(
8
+ resource,
9
+ query_hash
10
+ ).raise_unless_filter_group_requirements_met!
11
+ end
12
+
6
13
  if missing_required_filters.any? && !@opts[:bypass_required_filters]
7
14
  raise Errors::RequiredFilter.new(resource, missing_required_filters)
8
15
  end
@@ -31,7 +38,7 @@ module Graphiti
31
38
 
32
39
  def filter_via_adapter(filter, operator, value)
33
40
  type_name = Types.name_for(filter.values.first[:type])
34
- method = :"filter_#{type_name}_#{operator}"
41
+ method = :"filter_#{type_name}_#{operator}"
35
42
  attribute = filter.keys.first
36
43
 
37
44
  if resource.adapter.respond_to?(method)
@@ -54,6 +61,8 @@ module Graphiti
54
61
  unless type[:canonical_name] == :hash || !value.is_a?(String)
55
62
  value = parse_string_value(filter.values[0], value)
56
63
  end
64
+
65
+ check_deny_empty_filters!(resource, filter, value)
57
66
  value = parse_string_null(filter.values[0], value)
58
67
  validate_singular(resource, filter, value)
59
68
  value = coerce_types(filter.values[0], param_name.to_sym, value)
@@ -82,11 +91,11 @@ module Graphiti
82
91
  type = Types[filter.values[0][:type]][:canonical_name]
83
92
  if param_value.is_a?(Hash) && type == :hash
84
93
  operators_keys = filter.values[0][:operators].keys
85
- unless param_value.keys.all? {|k| operators_keys.include?(k)}
86
- param_value = { eq: param_value }
94
+ unless param_value.keys.all? { |k| operators_keys.include?(k) }
95
+ param_value = {eq: param_value}
87
96
  end
88
97
  elsif !param_value.is_a?(Hash) || param_value.empty?
89
- param_value = { eq: param_value }
98
+ param_value = {eq: param_value}
90
99
  end
91
100
 
92
101
  param_value.map do |operator, value|
@@ -162,14 +171,18 @@ module Graphiti
162
171
  type = Graphiti::Types[filter[:type]]
163
172
  array_or_string = [:string, :array].include?(type[:canonical_name])
164
173
  if (arr = value.scan(/\[.*?\]/)).present? && array_or_string
165
- value = arr.map { |json|
166
- begin
167
- JSON.parse(json)
168
- rescue
169
- raise Errors::InvalidJSONArray.new(resource, value)
170
- end
171
- }
172
- value = value[0] if value.length == 1
174
+ begin
175
+ value = arr.map { |json|
176
+ begin
177
+ JSON.parse(json)
178
+ rescue
179
+ raise Errors::InvalidJSONArray.new(resource, value)
180
+ end
181
+ }
182
+ value = value[0] if value.length == 1
183
+ rescue Errors::InvalidJSONArray => e
184
+ raise(e) if type[:canonical_name] == :array
185
+ end
173
186
  else
174
187
  value = parse_string_arrays(value, !!filter[:single])
175
188
  end
@@ -184,10 +197,10 @@ module Graphiti
184
197
  # remove the quote characters from the quoted strings
185
198
  quotes.each { |q| q.gsub!("{{", "").gsub!("}}", "") }
186
199
  # merge everything back together into an array
187
- if singular_filter
188
- value = Array(value) + quotes
200
+ value = if singular_filter
201
+ Array(value) + quotes
189
202
  else
190
- value = Array(value.split(",")) + quotes
203
+ Array(value.split(",")) + quotes
191
204
  end
192
205
  # remove any blanks that are left
193
206
  value.reject! { |v| v.length.zero? }
@@ -200,5 +213,13 @@ module Graphiti
200
213
 
201
214
  value
202
215
  end
216
+
217
+ def check_deny_empty_filters!(resource, filter, value)
218
+ return unless filter.values[0][:deny_empty]
219
+
220
+ if value.nil? || value.empty? || value == "null"
221
+ raise Errors::InvalidFilterValue.new(resource, filter, "(empty)")
222
+ end
223
+ end
203
224
  end
204
225
  end
@@ -0,0 +1,78 @@
1
+ module Graphiti
2
+ class Scoping::FilterGroupValidator
3
+ VALID_REQUIRED_VALUES = %i[all any]
4
+
5
+ def self.raise_unless_filter_group_requirement_valid!(resource, requirement)
6
+ unless VALID_REQUIRED_VALUES.include?(requirement)
7
+ raise Errors::FilterGroupInvalidRequirement.new(
8
+ resource,
9
+ VALID_REQUIRED_VALUES
10
+ )
11
+ end
12
+
13
+ true
14
+ end
15
+
16
+ def initialize(resource, query_hash)
17
+ @resource = resource
18
+ @query_hash = query_hash
19
+ end
20
+
21
+ def raise_unless_filter_group_requirements_met!
22
+ return if grouped_filters.empty?
23
+
24
+ case filter_group_requirement
25
+ when :all
26
+ raise_unless_all_requirements_met!
27
+ when :any
28
+ raise_unless_any_requirements_met!
29
+ end
30
+
31
+ true
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :resource, :query_hash
37
+
38
+ def raise_unless_all_requirements_met!
39
+ met = filter_group_names.all? { |filter_name| filter_group_filter_param.key?(filter_name) }
40
+
41
+ unless met
42
+ raise Errors::FilterGroupMissingRequiredFilters.new(
43
+ resource,
44
+ filter_group_names,
45
+ filter_group_requirement
46
+ )
47
+ end
48
+ end
49
+
50
+ def raise_unless_any_requirements_met!
51
+ met = filter_group_names.any? { |filter_name| filter_group_filter_param.key?(filter_name) }
52
+
53
+ unless met
54
+ raise Errors::FilterGroupMissingRequiredFilters.new(
55
+ resource,
56
+ filter_group_names,
57
+ filter_group_requirement
58
+ )
59
+ end
60
+ end
61
+
62
+ def filter_group_names
63
+ grouped_filters.fetch(:names, [])
64
+ end
65
+
66
+ def filter_group_requirement
67
+ grouped_filters.fetch(:required, :invalid)
68
+ end
69
+
70
+ def grouped_filters
71
+ resource.grouped_filters
72
+ end
73
+
74
+ def filter_group_filter_param
75
+ query_hash.fetch(:filter, {})
76
+ end
77
+ end
78
+ end
@@ -1,6 +1,7 @@
1
1
  module Graphiti
2
2
  class Scoping::Paginate < Scoping::Base
3
3
  DEFAULT_PAGE_SIZE = 20
4
+ PARAMS = [:number, :size, :offset, :before, :after]
4
5
 
5
6
  def apply
6
7
  if size > resource.max_page_size
@@ -33,24 +34,67 @@ module Graphiti
33
34
 
34
35
  # Apply default pagination proc via the Resource adapter
35
36
  def apply_standard_scope
36
- resource.adapter.paginate(@scope, number, size)
37
+ meth = resource.adapter.method(:paginate)
38
+
39
+ if meth.arity == 4 # backwards-compat
40
+ resource.adapter.paginate(@scope, number, size, offset)
41
+ else
42
+ resource.adapter.paginate(@scope, number, size)
43
+ end
37
44
  end
38
45
 
39
46
  # Apply the custom pagination proc
40
47
  def apply_custom_scope
41
- resource.instance_exec(@scope, number, size, resource.context, &custom_scope)
48
+ resource.instance_exec \
49
+ @scope,
50
+ number,
51
+ size,
52
+ resource.context,
53
+ offset,
54
+ &custom_scope
42
55
  end
43
56
 
44
57
  private
45
58
 
46
59
  def requested?
47
- ![page_param[:size], page_param[:number]].all?(&:nil?)
60
+ !PARAMS.map { |p| page_param[p] }.all?(&:nil?)
48
61
  end
49
62
 
50
63
  def page_param
51
64
  @page_param ||= (query_hash[:page] || {})
52
65
  end
53
66
 
67
+ def offset
68
+ offset = nil
69
+
70
+ if (value = page_param[:offset])
71
+ offset = value.to_i
72
+ end
73
+
74
+ if before_cursor&.key?(:offset)
75
+ if page_param.key?(:number)
76
+ raise Errors::UnsupportedBeforeCursor
77
+ end
78
+
79
+ offset = before_cursor[:offset] - (size * number) - 1
80
+ offset = 0 if offset.negative?
81
+ end
82
+
83
+ if after_cursor&.key?(:offset)
84
+ offset = after_cursor[:offset]
85
+ end
86
+
87
+ offset
88
+ end
89
+
90
+ def after_cursor
91
+ page_param[:after]
92
+ end
93
+
94
+ def before_cursor
95
+ page_param[:before]
96
+ end
97
+
54
98
  def number
55
99
  (page_param[:number] || 1).to_i
56
100
  end
@@ -59,12 +59,10 @@ module Graphiti
59
59
  end
60
60
 
61
61
  def sort_param
62
- @sort_param ||= begin
63
- if query_hash[:sort].blank?
64
- resource.default_sort || []
65
- else
66
- normalize(query_hash[:sort])
67
- end
62
+ @sort_param ||= if query_hash[:sort].blank?
63
+ resource.default_sort || []
64
+ else
65
+ normalize(query_hash[:sort])
68
66
  end
69
67
  end
70
68
 
@@ -78,7 +76,7 @@ module Graphiti
78
76
 
79
77
  def sort_hash(attr)
80
78
  value = attr[0] == "-" ? :desc : :asc
81
- key = attr.sub("-", "").to_sym
79
+ key = attr.sub("-", "").to_sym
82
80
 
83
81
  {key => value}
84
82
  end
@@ -12,26 +12,62 @@ module Graphiti
12
12
  # go through type checking/coercion
13
13
  class_attribute :attributes_applied_via_resource
14
14
  class_attribute :extra_attributes_applied_via_resource
15
+ class_attribute :relationship_condition_blocks
15
16
  self.attributes_applied_via_resource = []
16
17
  self.extra_attributes_applied_via_resource = []
18
+ # See #requested_relationships
19
+ self.relationship_condition_blocks ||= {}
17
20
 
18
21
  def self.inherited(klass)
19
22
  super
20
23
  klass.class_eval do
21
24
  extend JSONAPI::Serializable::Resource::ConditionalFields
25
+
26
+ # See #requested_relationships
27
+ def self.relationship(name, options = {}, &block)
28
+ prev = Util::Hash.deep_dup(field_condition_blocks)
29
+ super
30
+ self.field_condition_blocks = prev
31
+ _register_condition(relationship_condition_blocks, name, options)
32
+ end
33
+
34
+ # NB - avoid clobbering includes when sparse fieldset
35
+ # https://github.com/jsonapi-rb/jsonapi-serializable/pull/102
36
+ #
37
+ # We also override this method to ensure attributes and relationships
38
+ # have separate condition blocks. This way an attribute and
39
+ # relationship can have the same name, and the attribute can be
40
+ # conditional without affecting the relationship.
41
+ def requested_relationships(fields)
42
+ @_relationships.select do |k, _|
43
+ _conditionally_included?(self.class.relationship_condition_blocks, k)
44
+ end
45
+ end
22
46
  end
23
47
  end
24
48
 
25
- def as_jsonapi(*)
26
- super.tap do |hash|
27
- strip_relationships!(hash) if strip_relationships?
49
+ def cursor
50
+ starting_offset = 0
51
+ page_param = @proxy.query.pagination
52
+ if (page_number = page_param[:number])
53
+ page_size = page_param[:size] || @resource.default_page_size
54
+ starting_offset = (page_number - 1) * page_size
28
55
  end
56
+
57
+ if (cursor = page_param[:after])
58
+ starting_offset = cursor[:offset]
59
+ end
60
+
61
+ current_offset = @object.instance_variable_get(:@__graphiti_index)
62
+ offset = starting_offset + current_offset + 1 # (+ 1 b/c o-base index)
63
+ Base64.encode64({offset: offset}.to_json).chomp
29
64
  end
30
65
 
31
- # Temporary fix until fixed upstream
32
- # https://github.com/jsonapi-rb/jsonapi-serializable/pull/102
33
- def requested_relationships(fields)
34
- @_relationships
66
+ def as_jsonapi(kwargs = {})
67
+ super(**kwargs).tap do |hash|
68
+ strip_relationships!(hash) if strip_relationships?
69
+ add_links!(hash)
70
+ end
35
71
  end
36
72
 
37
73
  # Allow access to resource methods
@@ -49,6 +85,12 @@ module Graphiti
49
85
 
50
86
  private
51
87
 
88
+ def add_links!(hash)
89
+ return unless @resource.respond_to?(:links?)
90
+
91
+ hash[:links] = @resource.links(@object) if @resource.links?
92
+ end
93
+
52
94
  def strip_relationships!(hash)
53
95
  hash[:relationships]&.select! do |name, payload|
54
96
  payload.key?(:data)
@@ -1,6 +1,6 @@
1
1
  class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
2
2
  def initialize(name, opts)
3
- opts = { always_include_resource_ids: false }.merge(opts)
3
+ opts = {always_include_resource_ids: false}.merge(opts)
4
4
  super(name, opts)
5
5
  end
6
6
 
@@ -1,8 +1,18 @@
1
1
  class Graphiti::Sideload::HasMany < Graphiti::Sideload
2
+ def initialize(name, opts)
3
+ @inverse_filter = opts[:inverse_filter]
4
+
5
+ super(name, opts)
6
+ end
7
+
2
8
  def type
3
9
  :has_many
4
10
  end
5
11
 
12
+ def inverse_filter
13
+ @inverse_filter || foreign_key
14
+ end
15
+
6
16
  def load_params(parents, query)
7
17
  query.hash.tap do |hash|
8
18
  hash[:filter] ||= {}
@@ -11,11 +21,19 @@ class Graphiti::Sideload::HasMany < Graphiti::Sideload
11
21
  end
12
22
 
13
23
  def base_filter(parents)
14
- {foreign_key => ids_for_parents(parents).join(",")}
24
+ {foreign_key => parent_filter(parents)}
25
+ end
26
+
27
+ def link_filter(parents)
28
+ {inverse_filter => parent_filter(parents)}
15
29
  end
16
30
 
17
31
  private
18
32
 
33
+ def parent_filter(parents)
34
+ ids_for_parents(parents).join(",")
35
+ end
36
+
19
37
  def child_map(children)
20
38
  children.group_by(&foreign_key)
21
39
  end
@@ -11,8 +11,12 @@ class Graphiti::Sideload::ManyToMany < Graphiti::Sideload::HasMany
11
11
  foreign_key.values.first
12
12
  end
13
13
 
14
+ def inverse_filter
15
+ @inverse_filter || true_foreign_key
16
+ end
17
+
14
18
  def base_filter(parents)
15
- {true_foreign_key => ids_for_parents(parents).join(",")}
19
+ {true_foreign_key => parent_filter(parents)}
16
20
  end
17
21
 
18
22
  def infer_foreign_key
@@ -32,9 +36,12 @@ class Graphiti::Sideload::ManyToMany < Graphiti::Sideload::HasMany
32
36
  self_ref = self
33
37
  fk_type = parent_resource_class.attributes[:id][:type]
34
38
  fk_type = :hash if polymorphic?
35
- resource_class.filter true_foreign_key, fk_type do
36
- eq do |scope, value|
37
- self_ref.belongs_to_many_filter(scope, value)
39
+ # Do not recreate if filter already exists
40
+ unless resource_class.config[:filters].has_key?(inverse_filter.to_sym)
41
+ resource_class.filter inverse_filter, fk_type do
42
+ eq do |scope, value|
43
+ self_ref.belongs_to_many_filter(scope, value)
44
+ end
38
45
  end
39
46
  end
40
47
  end
@@ -7,7 +7,6 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
7
7
  @calls = []
8
8
  end
9
9
 
10
- # rubocop: disable Style/MethodMissingSuper
11
10
  def method_missing(name, *args, &blk)
12
11
  @calls << [name, args, blk]
13
12
  end
@@ -56,9 +55,9 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
56
55
  args = call[1]
57
56
  opts = args.extract_options!
58
57
  opts.merge! as: sideload.name,
59
- parent: sideload,
60
- group_name: group.name,
61
- polymorphic_child: true
58
+ parent: sideload,
59
+ group_name: group.name,
60
+ polymorphic_child: true
62
61
  unless sideload.resource.class.abstract_class?
63
62
  opts[:foreign_key] ||= sideload.foreign_key
64
63
  opts[:primary_key] ||= sideload.primary_key
@@ -18,31 +18,31 @@ module Graphiti
18
18
  :link_proc
19
19
 
20
20
  def initialize(name, opts)
21
- @name = name
21
+ @name = name
22
22
  validate_options!(opts)
23
- @parent_resource_class = opts[:parent_resource]
24
- @resource_class = opts[:resource]
25
- @primary_key = opts[:primary_key]
26
- @foreign_key = opts[:foreign_key]
27
- @type = opts[:type]
28
- @base_scope = opts[:base_scope]
29
- @readable = evaluate_flag(opts[:readable])
30
- @writable = evaluate_flag(opts[:writable])
31
- @as = opts[:as]
32
- @link = opts[:link]
33
- @single = opts[:single]
34
- @remote = opts[:remote]
23
+ @parent_resource_class = opts[:parent_resource]
24
+ @resource_class = opts[:resource]
25
+ @primary_key = opts[:primary_key]
26
+ @foreign_key = opts[:foreign_key]
27
+ @type = opts[:type]
28
+ @base_scope = opts[:base_scope]
29
+ @readable = opts[:readable]
30
+ @writable = opts[:writable]
31
+ @as = opts[:as]
32
+ @link = opts[:link]
33
+ @single = opts[:single]
34
+ @remote = opts[:remote]
35
35
  apply_belongs_to_many_filter if type == :many_to_many
36
36
 
37
- @description = opts[:description]
37
+ @description = opts[:description]
38
38
 
39
39
  # polymorphic has_many
40
- @polymorphic_as = opts[:polymorphic_as]
40
+ @polymorphic_as = opts[:polymorphic_as]
41
41
  # polymorphic_belongs_to-specific
42
- @group_name = opts[:group_name]
43
- @polymorphic_child = opts[:polymorphic_child]
44
- @parent = opts[:parent]
45
- @always_include_resource_ids = opts[:always_include_resource_ids]
42
+ @group_name = opts[:group_name]
43
+ @polymorphic_child = opts[:polymorphic_child]
44
+ @parent = opts[:parent]
45
+ @always_include_resource_ids = opts[:always_include_resource_ids]
46
46
 
47
47
  if polymorphic_child?
48
48
  parent.resource.polymorphic << resource_class
@@ -132,6 +132,23 @@ module Graphiti
132
132
  end
133
133
  end
134
134
 
135
+ def link_filter(parents)
136
+ base_filter(parents)
137
+ end
138
+
139
+ def link_extra_fields
140
+ return unless context&.respond_to?(:params)
141
+
142
+ extra_fields_name = [association_name, resource.type].find { |param|
143
+ context.params.dig(:extra_fields, param)
144
+ }
145
+
146
+ if extra_fields_name
147
+ extra_fields = context.params.dig(:extra_fields, extra_fields_name)
148
+ {resource.type => extra_fields}
149
+ end
150
+ end
151
+
135
152
  # The parent resource is a remote,
136
153
  # AND the sideload is a remote to the same endpoint
137
154
  def shared_remote?
@@ -197,7 +214,7 @@ module Graphiti
197
214
 
198
215
  with_error_handling Errors::SideloadParamsError do
199
216
  params = load_params(parents, query)
200
- params_proc&.call(params, parents)
217
+ params_proc&.call(params, parents, context)
201
218
  return [] if blank_query?(params)
202
219
  opts = load_options(parents, query)
203
220
  opts[:sideload] = self
@@ -205,7 +222,9 @@ module Graphiti
205
222
  end
206
223
 
207
224
  with_error_handling(Errors::SideloadQueryBuildingError) do
208
- proxy = resource.class._all(params, opts, base_scope)
225
+ scope = base_scope
226
+ scope[:foreign_key] = foreign_key if remote?
227
+ proxy = resource.class._all(params, opts, scope)
209
228
  pre_load_proc&.call(proxy, parents)
210
229
  end
211
230
 
@@ -422,17 +441,22 @@ module Graphiti
422
441
  Util::Class.namespace_for(klass)
423
442
  end
424
443
 
444
+ # TODO: call this at runtime to support procs
425
445
  def evaluate_flag(flag)
426
446
  return false if flag.blank?
427
447
 
428
448
  case flag.class.name
429
- when "Symbol","String"
449
+ when "Symbol", "String"
430
450
  resource.send(flag)
431
451
  when "Proc"
432
- self.resource.instance_exec(&flag)
452
+ resource.instance_exec(&flag)
433
453
  else
434
454
  !!flag
435
455
  end
436
456
  end
457
+
458
+ def context
459
+ Graphiti.context[:object]
460
+ end
437
461
  end
438
462
  end
@@ -46,7 +46,6 @@ module Graphiti
46
46
  #
47
47
  # ...will hit +method_missing+ and store the proc for future reference.
48
48
  # @api private
49
- # rubocop: disable Style/MethodMissingSuper
50
49
  def method_missing(meth, *args, &blk)
51
50
  @calculations[meth] = blk
52
51
  end