graphiti 1.2.16 → 1.3.9

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.
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