graphiti 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +7 -0
  3. data/CHANGELOG.md +13 -0
  4. data/bin/cut-docs-version +24 -0
  5. data/lib/generators/graphiti/generator_mixin.rb +1 -1
  6. data/lib/generators/graphiti/templates/application_resource.rb.erb +5 -5
  7. data/lib/graphiti/adapters/abstract.rb +8 -0
  8. data/lib/graphiti/adapters/active_record.rb +8 -0
  9. data/lib/graphiti/adapters/persistence/associations.rb +6 -1
  10. data/lib/graphiti/audit/report.rb +3 -1
  11. data/lib/graphiti/audit.rb +14 -1
  12. data/lib/graphiti/errors.rb +36 -0
  13. data/lib/graphiti/query.rb +7 -0
  14. data/lib/graphiti/request_validators/validator.rb +10 -1
  15. data/lib/graphiti/resource/configuration.rb +124 -4
  16. data/lib/graphiti/resource/dsl.rb +47 -0
  17. data/lib/graphiti/resource/interface.rb +39 -0
  18. data/lib/graphiti/resource/persistence.rb +29 -0
  19. data/lib/graphiti/resource/sideloading.rb +2 -0
  20. data/lib/graphiti/resource.rb +4 -0
  21. data/lib/graphiti/resource_proxy.rb +6 -3
  22. data/lib/graphiti/runner.rb +3 -1
  23. data/lib/graphiti/schema.rb +4 -0
  24. data/lib/graphiti/schema_diff.rb +5 -0
  25. data/lib/graphiti/scope.rb +8 -0
  26. data/lib/graphiti/scoping/default_filter.rb +2 -0
  27. data/lib/graphiti/scoping/filter.rb +79 -20
  28. data/lib/graphiti/scoping/sort.rb +1 -1
  29. data/lib/graphiti/sideload/belongs_to.rb +52 -8
  30. data/lib/graphiti/sideload/has_many.rb +33 -2
  31. data/lib/graphiti/sideload/many_to_many.rb +3 -3
  32. data/lib/graphiti/sideload.rb +25 -1
  33. data/lib/graphiti/stats/payload.rb +20 -3
  34. data/lib/graphiti/util/internal_param.rb +16 -0
  35. data/lib/graphiti/util/link.rb +10 -3
  36. data/lib/graphiti/util/persistence.rb +1 -1
  37. data/lib/graphiti/util/public_id_block.rb +15 -0
  38. data/lib/graphiti/util/public_id_map.rb +34 -0
  39. data/lib/graphiti/util/public_id_sources.rb +27 -0
  40. data/lib/graphiti/util/serializer_relationships.rb +5 -3
  41. data/lib/graphiti/version.rb +1 -1
  42. data/lib/graphiti.rb +16 -0
  43. metadata +6 -1
@@ -150,6 +150,10 @@ module Graphiti
150
150
  persistence.run
151
151
  end
152
152
 
153
+ def model_attribute_for(name)
154
+ self.class.model_attribute_for(name)
155
+ end
156
+
153
157
  def stat(attribute, calculation)
154
158
  stats_dsl = stats[attribute] || stats[attribute.to_sym]
155
159
  raise Errors::StatNotFound.new(attribute, calculation) unless stats_dsl
@@ -125,14 +125,17 @@ module Graphiti
125
125
  scope = @scope.unpaginated_object
126
126
  if resource.adapter.can_group?
127
127
  if (group = @query.hash[:stats].delete(:group_by))
128
- resource.get_attr!(group[0], :readable, request: true)
129
- scope = resource.adapter.group(scope, group[0])
128
+ # A foreign key whose values render as public ids exposes nothing, so it may group even when unreadable.
129
+ flag = resource.class.public_id_source_for(group[0]) ? :filterable : :readable
130
+ resource.get_attr!(group[0], flag, request: true)
131
+ scope = resource.adapter.group(scope, resource.model_attribute_for(group[0]))
130
132
  end
131
133
  end
132
134
  payload = Stats::Payload.new @resource,
133
135
  @query,
134
136
  scope,
135
- data
137
+ data,
138
+ group_by: group&.first
136
139
  payload.generate
137
140
  else
138
141
  {}
@@ -70,7 +70,9 @@ module Graphiti
70
70
  :sideload,
71
71
  :parent,
72
72
  :params,
73
- :bypass_required_filters
73
+ :bypass_required_filters,
74
+ :bypass_default_filters,
75
+ :translating_public_ids
74
76
  )
75
77
 
76
78
  scope = jsonapi_scope(base, scope_opts)
@@ -103,6 +103,10 @@ module Graphiti
103
103
  stats: stats(r)
104
104
  }
105
105
 
106
+ if r.publishes_public_id?
107
+ config[:public_id] = r.config[:public_id]&.to_s || true
108
+ end
109
+
106
110
  if r.grouped_filters.any?
107
111
  config[:filter_group] = r.grouped_filters
108
112
  end
@@ -46,6 +46,11 @@ module Graphiti
46
46
  if old_resource[:type] != new_resource[:type]
47
47
  @errors << "#{old_resource[:name]} changed type from #{old_resource[:type].inspect} to #{new_resource[:type].inspect}."
48
48
  end
49
+
50
+ # Every id a client holds, and every url built from one, stops resolving.
51
+ if old_resource[:public_id] != new_resource[:public_id]
52
+ @errors << "#{old_resource[:name]} changed public_id from #{old_resource[:public_id].inspect} to #{new_resource[:public_id].inspect}."
53
+ end
49
54
  yield
50
55
  end
51
56
 
@@ -153,6 +153,7 @@ module Graphiti
153
153
  def sync_resolve_sideloads(results)
154
154
  return if results == []
155
155
 
156
+ collect_foreign_keys(results)
156
157
  reset_captured_sideload_proxies
157
158
  each_applicable_sideload do |name, sideload, sideload_query|
158
159
  Graphiti.config.before_sideload&.call(Graphiti.context)
@@ -221,6 +222,12 @@ module Graphiti
221
222
  false
222
223
  end
223
224
 
225
+ def collect_foreign_keys(results)
226
+ return unless Graphiti.public_ids_declared? && !@opts[:translating_public_ids]
227
+
228
+ @resource.class.sideloads.each_value { |sideload| sideload.collect_foreign_keys(results, @query) }
229
+ end
230
+
224
231
  def each_applicable_sideload
225
232
  @query.sideloads.each_pair do |name, sideload_query|
226
233
  sideload = @resource.class.sideload(name)
@@ -246,6 +253,7 @@ module Graphiti
246
253
  def future_resolve_sideloads(results)
247
254
  return Concurrent::Promises.fulfilled_future(nil, self.class.global_thread_pool_executor) if results == []
248
255
 
256
+ collect_foreign_keys(results)
249
257
  reset_captured_sideload_proxies
250
258
  sideload_promises = []
251
259
  each_applicable_sideload do |name, sideload, sideload_query|
@@ -36,6 +36,8 @@ module Graphiti
36
36
  #
37
37
  # @return scope the scope object we are chaining/modifying
38
38
  def apply
39
+ return @scope if @opts[:bypass_default_filters]
40
+
39
41
  resource.default_filters.each_pair do |name, opts|
40
42
  next if overridden?(name)
41
43
  @scope = resource.instance_exec(@scope, resource.context, &opts[:filter])
@@ -19,8 +19,8 @@ module Graphiti
19
19
  resource, missing_dependent_filters
20
20
  end
21
21
 
22
- each_filter do |filter, operator, value|
23
- @scope = filter_scope(filter, operator, value)
22
+ each_filter do |filter, operator, value, primary_keys, decoded|
23
+ @scope = filter_scope(filter, operator, value, primary_keys, decoded)
24
24
  end
25
25
 
26
26
  resource.after_filtering(@scope)
@@ -28,18 +28,24 @@ module Graphiti
28
28
 
29
29
  private
30
30
 
31
- def filter_scope(filter, operator, value)
31
+ def filter_scope(filter, operator, value, primary_keys, decoded)
32
32
  if (custom_scope = filter.values[0][:operators][operator])
33
- @resource.instance_exec(@scope, value, resource.context, &custom_scope)
33
+ if takes_primary_keys?(filter, operator)
34
+ @resource.instance_exec(@scope, value, resource.context, primary_keys: primary_keys, &custom_scope)
35
+ else
36
+ @resource.instance_exec(@scope, value, resource.context, &custom_scope)
37
+ end
34
38
  else
35
- filter_via_adapter(filter, operator, value)
39
+ filter_via_adapter(filter, operator, value, decoded)
36
40
  end
37
41
  end
38
42
 
39
- def filter_via_adapter(filter, operator, value)
40
- type_name = Types.name_for(filter.values.first[:type])
43
+ # A foreign key filter holds primary keys once its public ids are decoded, whatever type the attribute renders as.
44
+ def filter_via_adapter(filter, operator, value, decoded = false)
45
+ type_name = Types.name_for(decoded ? :integer_id : filter.values.first[:type])
46
+ type_name = :public_id if type_name == :string && [:eq, :not_eq].include?(operator) && resource.class.public_id_attribute?(filter.keys.first)
41
47
  method = :"filter_#{type_name}_#{operator}"
42
- attribute = filter.keys.first
48
+ attribute = resource.model_attribute_for(filter.keys.first)
43
49
 
44
50
  if resource.adapter.respond_to?(method)
45
51
  resource.adapter.send(method, @scope, attribute, value)
@@ -53,27 +59,80 @@ module Graphiti
53
59
  filter_param.each_pair do |param_name, param_value|
54
60
  filter = find_filter!(param_name)
55
61
 
62
+ internal = param_value.is_a?(Util::InternalParam)
63
+ if internal
64
+ param_value = param_value.value
65
+ elsif filter.values[0][:internal]
66
+ raise Errors::InvalidAttributeAccess.new(resource, param_name, :filterable, request: true)
67
+ else
68
+ source = resource.class.public_id_source_for(filter.keys[0])
69
+ end
70
+
56
71
  normalize_param(filter, param_value).each do |operator, value|
57
72
  operator = operator.to_s.gsub("!", "not_").to_sym
58
73
  validate_operator(filter, operator)
74
+ custom = filter.values[0][:operators][operator]
75
+ decoded = !!source && !custom
76
+ value = decode_public_ids(source, value) if decoded
59
77
 
60
- type = Types[filter.values[0][:type]]
61
- unless type[:canonical_name] == :hash || !value.is_a?(String)
62
- value = parse_string_value(filter.values[0], value)
63
- end
64
-
65
- check_blank_filters!(resource, filter, value)
66
- value = parse_string_null(filter.values[0], value)
67
- validate_singular(resource, filter, value)
68
- value = coerce_types(filter.values[0], param_name.to_sym, value)
69
- validate_allowlist(resource, filter, value)
70
- validate_denylist(resource, filter, value)
78
+ decoding = source && takes_primary_keys?(filter, operator)
79
+ value = validated_and_cast(filter, param_name, value, decoding ? source : nil) unless internal
71
80
  value = value[0] if filter.values[0][:single]
72
- yield filter, operator, value
81
+ primary_keys = decoded_for_block(filter, operator, source, value)
82
+ yield filter, operator, value, primary_keys, decoded
73
83
  end
74
84
  end
75
85
  end
76
86
 
87
+ # A block asking for primary_keys: was sent public ids, which cast like the source's id rather than the key.
88
+ def validated_and_cast(filter, param_name, value, public_id_source)
89
+ type = Types[filter.values[0][:type]]
90
+ unless type[:canonical_name] == :hash || !value.is_a?(String)
91
+ value = parse_string_value(filter.values[0], value)
92
+ end
93
+
94
+ check_blank_filters!(resource, filter, value)
95
+ value = parse_string_null(filter.values[0], value)
96
+ validate_singular(resource, filter, value)
97
+ value = public_id_source ? cast_as_public_ids(public_id_source, value) : coerce_types(filter.values[0], param_name.to_sym, value)
98
+ validate_allowlist(resource, filter, value)
99
+ validate_denylist(resource, filter, value)
100
+ value
101
+ end
102
+
103
+ def cast_as_public_ids(source, value)
104
+ source_instance = source.new
105
+ Array(value).map { |public_id| source_instance.typecast(:id, public_id, :filterable) }
106
+ end
107
+
108
+ def takes_primary_keys?(filter, operator)
109
+ Array(filter.values[0][:operators_taking_primary_keys]).include?(operator)
110
+ end
111
+
112
+ def decoded_for_block(filter, operator, source, value)
113
+ return unless takes_primary_keys?(filter, operator)
114
+ source ||= resource.class if filter.keys[0] == :id && resource.class.publishes_public_id?
115
+ return value unless source
116
+
117
+ keys = decode_public_ids(source, value)
118
+ filter.values[0][:single] ? keys.first : keys
119
+ end
120
+
121
+ def decode_public_ids(source_resource_class, value)
122
+ case value
123
+ when Hash
124
+ value.transform_values { |operand| decode_public_ids(source_resource_class, operand) }
125
+ when Array
126
+ lookup_primary_keys(source_resource_class, value)
127
+ else
128
+ lookup_primary_keys(source_resource_class, value.to_s.split(","))
129
+ end
130
+ end
131
+
132
+ def lookup_primary_keys(source_resource_class, public_ids)
133
+ source_resource_class.decode_public_ids(public_ids)
134
+ end
135
+
77
136
  def coerce_types(filter, name, value)
78
137
  type_name = filter[:type]
79
138
  is_array = type_name.to_s.starts_with?("array_of") ||
@@ -31,7 +31,7 @@ module Graphiti
31
31
  @scope = if sort[:proc]
32
32
  resource.instance_exec(@scope, direction, &sort[:proc])
33
33
  else
34
- resource.adapter.order(@scope, attribute, direction)
34
+ resource.adapter.order(@scope, resource.model_attribute_for(attribute), direction)
35
35
  end
36
36
  end
37
37
  end
@@ -17,7 +17,7 @@ class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
17
17
 
18
18
  def resource_ids_blocker
19
19
  return :unreadable unless renderable_at_all?
20
- return :custom_primary_key unless foreign_key_is_related_id?
20
+ return :custom_primary_key unless foreign_key_is_related_id? || resolves_public_ids?
21
21
  return :polymorphic_child if polymorphic_child?
22
22
  return :scope_block if self.class.scope_proc
23
23
  return :params_block if self.class.params_proc
@@ -28,12 +28,6 @@ class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
28
28
  nil
29
29
  end
30
30
 
31
- # base_filter matches the foreign key against primary_key, so a custom
32
- # primary_key means the key holds that column's value, not the related id.
33
- def foreign_key_is_related_id?
34
- primary_key == :id
35
- end
36
-
37
31
  def load_params(parents, query)
38
32
  query.hash.tap do |hash|
39
33
  hash[:filter] ||= {}
@@ -43,7 +37,49 @@ class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
43
37
 
44
38
  def base_filter(parents)
45
39
  parent_ids = ids_for_parents(parents)
46
- {primary_key => parent_ids.join(",")}
40
+ return {primary_key_filter => Graphiti::Util::InternalParam.new(parent_ids)} if primary_key_filter == :_primary_key
41
+
42
+ {primary_key_filter => parent_ids.join(",")}
43
+ end
44
+
45
+ def resolves_public_ids?
46
+ @resolves_public_ids ||= target_publishes_id? && resource_class.filters.key?(primary_key_filter)
47
+ end
48
+
49
+ def link_hides_primary_key?
50
+ !target_publishes_id? || resolves_public_ids?
51
+ end
52
+
53
+ def register_public_id_source
54
+ return unless target_publishes_id? && resource_class_loaded? && parent_resource_class.model_declared?
55
+
56
+ parent_resource_class.guard_public_id_leak!(foreign_key)
57
+ end
58
+
59
+ def rendered_id_for(foreign_key, query)
60
+ return foreign_key unless target_publishes_id?
61
+ return unless resolves_public_ids?
62
+
63
+ public_id_map(query)[foreign_key]
64
+ end
65
+
66
+ # The serializer reports a foreign key the record cannot answer with a better error.
67
+ def collect_foreign_keys(parents, query)
68
+ return unless resource_class_loaded? && resolves_public_ids?
69
+ return unless parents.first.respond_to?(foreign_key)
70
+
71
+ public_id_map(query).add(ids_for_parents(parents))
72
+ end
73
+
74
+ def primary_key_filter
75
+ return primary_key unless target_publishes_id?
76
+ return :_primary_key if primary_key == :id || primary_key.to_s == resource_class.model_primary_key.to_s
77
+
78
+ primary_key
79
+ end
80
+
81
+ def foreign_key_is_related_id?
82
+ primary_key_filter == :id
47
83
  end
48
84
 
49
85
  def ids_for_parents(parents)
@@ -75,6 +111,14 @@ class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
75
111
 
76
112
  private
77
113
 
114
+ def target_publishes_id?
115
+ @target_publishes_id ||= Graphiti.public_ids_declared? && resource_class.publishes_public_id?
116
+ end
117
+
118
+ def public_id_map(query)
119
+ query.public_id_maps.compute_if_absent(self) { Graphiti::Util::PublicIdMap.new(resource_class, primary_key_filter) }
120
+ end
121
+
78
122
  def child_map(children)
79
123
  children.index_by(&primary_key)
80
124
  end
@@ -21,11 +21,30 @@ class Graphiti::Sideload::HasMany < Graphiti::Sideload
21
21
  end
22
22
 
23
23
  def base_filter(parents)
24
- {foreign_key => parent_filter(parents)}
24
+ {foreign_key => internal_parent_filter(parents, foreign_key)}
25
25
  end
26
26
 
27
27
  def link_filter(parents)
28
- {inverse_filter => parent_filter(parents)}
28
+ {inverse_filter => public_parent_filter(parents)}
29
+ end
30
+
31
+ def link_hides_primary_key?
32
+ return true unless links_by_public_id?
33
+
34
+ resource_class.public_id_source_for(inverse_filter) == parent_resource_class &&
35
+ resource_class.filter_accepts_public_ids?(inverse_filter)
36
+ end
37
+
38
+ def links_by_public_id?
39
+ @links_by_public_id ||= Graphiti.public_ids_declared? && primary_key == :id && !!parent_resource_class&.publishes_public_id?
40
+ end
41
+
42
+ def register_public_id_source
43
+ return unless links_by_public_id? && resource_class_loaded?
44
+ return if parent_resource_class.abstract_class? || !parent_resource_class.model_loaded?
45
+
46
+ Graphiti.public_id_sources.register(resource_class, inverse_filter, parent_resource_class)
47
+ resource_class.guard_public_id_leak!(inverse_filter)
29
48
  end
30
49
 
31
50
  private
@@ -34,6 +53,18 @@ class Graphiti::Sideload::HasMany < Graphiti::Sideload
34
53
  ids_for_parents(parents).join(",")
35
54
  end
36
55
 
56
+ def internal_parent_filter(parents, filter_name)
57
+ return parent_filter(parents) unless resource_class.public_id_source_for(filter_name)
58
+
59
+ Graphiti::Util::InternalParam.new(ids_for_parents(parents))
60
+ end
61
+
62
+ def public_parent_filter(parents)
63
+ return parent_filter(parents) unless links_by_public_id?
64
+
65
+ parents.map { |parent| parent_resource_class.public_id_for(parent) }.compact.uniq.join(",")
66
+ end
67
+
37
68
  def child_map(children)
38
69
  children.group_by(&foreign_key)
39
70
  end
@@ -16,7 +16,7 @@ class Graphiti::Sideload::ManyToMany < Graphiti::Sideload::HasMany
16
16
  end
17
17
 
18
18
  def base_filter(parents)
19
- {true_foreign_key => parent_filter(parents)}
19
+ {true_foreign_key => internal_parent_filter(parents, true_foreign_key)}
20
20
  end
21
21
 
22
22
  def infer_foreign_key
@@ -39,8 +39,8 @@ class Graphiti::Sideload::ManyToMany < Graphiti::Sideload::HasMany
39
39
  # Do not recreate if filter already exists
40
40
  unless resource_class.config[:filters].has_key?(inverse_filter.to_sym)
41
41
  resource_class.filter inverse_filter, fk_type do
42
- eq do |scope, value|
43
- self_ref.belongs_to_many_filter(scope, value)
42
+ eq do |scope, value, primary_keys:|
43
+ self_ref.belongs_to_many_filter(scope, primary_keys)
44
44
  end
45
45
  end
46
46
  end
@@ -198,8 +198,18 @@ module Graphiti
198
198
 
199
199
  # A custom link block means the author wants the link, so a false default does not silence it.
200
200
  def link_mode
201
- return @link unless @link.nil?
201
+ mode = requested_link_mode
202
+ return false unless mode
203
+ return false unless link_proc || link_hides_primary_key?
202
204
 
205
+ mode
206
+ end
207
+
208
+ def requested_link_mode
209
+ @link.nil? ? default_link_mode : @link
210
+ end
211
+
212
+ def default_link_mode
203
213
  default = @parent_resource_class.relationship_links
204
214
  (link_proc && default == false) ? true : default
205
215
  end
@@ -208,6 +218,20 @@ module Graphiti
208
218
  link_mode != false
209
219
  end
210
220
 
221
+ def link_hides_primary_key?
222
+ true
223
+ end
224
+
225
+ def collect_foreign_keys(parents, query)
226
+ end
227
+
228
+ def register_public_id_source
229
+ end
230
+
231
+ def rendered_id_for(foreign_key, query)
232
+ foreign_key
233
+ end
234
+
211
235
  def link_filter(parents)
212
236
  base_filter(parents)
213
237
  end
@@ -14,11 +14,12 @@ module Graphiti
14
14
  # meta: { stats: { total: { count: 100 } } }
15
15
  # }
16
16
  class Payload
17
- def initialize(resource, query, scope, data)
17
+ def initialize(resource, query, scope, data, group_by: nil)
18
18
  @resource = resource
19
19
  @query = query
20
20
  @scope = scope
21
21
  @data = data
22
+ @group_by = group_by
22
23
  end
23
24
 
24
25
  # Generate the payload for +{ meta: { stats: { ... } } }+
@@ -38,14 +39,30 @@ module Graphiti
38
39
  end
39
40
 
40
41
  def calculate_stat(name, function)
41
- args = [@scope, name]
42
+ args = [@scope, @resource.model_attribute_for(name)]
42
43
  args << @resource.context if function.arity >= 3
43
44
  args << @data if function.arity == 4
44
- function.call(*args)
45
+ translate_group_keys(function.call(*args))
45
46
  end
46
47
 
47
48
  private
48
49
 
50
+ def translate_group_keys(result)
51
+ return result unless result.is_a?(Hash) && @group_by
52
+
53
+ source = @resource.class.public_id_source_for(@group_by)
54
+ return result unless source
55
+
56
+ public_ids = source.public_ids_by(:_primary_key, result.keys.compact)
57
+ result.each_with_object({}) do |(primary_key, value), translated|
58
+ if primary_key.nil?
59
+ translated[nil] = value
60
+ elsif public_ids.key?(primary_key)
61
+ translated[public_ids[primary_key]] = value
62
+ end
63
+ end
64
+ end
65
+
49
66
  def each_calculation(name, calculations)
50
67
  calculations.each do |calc|
51
68
  function = @resource.stat(name, calc)
@@ -0,0 +1,16 @@
1
+ module Graphiti
2
+ module Util
3
+ # Request params can only ever be strings, arrays and hashes, so a value wrapped in this class must have come from Graphiti itself.
4
+ class InternalParam
5
+ attr_reader :value
6
+
7
+ def initialize(value)
8
+ @value = value
9
+ end
10
+
11
+ def blank?
12
+ value.blank?
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,9 +1,10 @@
1
1
  module Graphiti
2
2
  module Util
3
3
  class Link
4
- def initialize(sideload, model)
4
+ def initialize(sideload, model, query = nil)
5
5
  @sideload = sideload
6
6
  @model = model
7
+ @query = query
7
8
  @linkable = true
8
9
 
9
10
  if @sideload.type == :polymorphic_belongs_to
@@ -30,12 +31,18 @@ module Graphiti
30
31
  return false if @polymorphic_sideload_not_found
31
32
 
32
33
  if @sideload.type == :belongs_to
33
- !@model.send(@sideload.foreign_key).nil?
34
+ !related_id.nil?
34
35
  else
35
36
  @linkable
36
37
  end
37
38
  end
38
39
 
40
+ def related_id
41
+ return @related_id if defined?(@related_id)
42
+
43
+ @related_id = @sideload.rendered_id_for(@model.send(@sideload.foreign_key), @query)
44
+ end
45
+
39
46
  def raw_url
40
47
  if @sideload.link_proc
41
48
  @sideload.link_proc.call(@model)
@@ -76,7 +83,7 @@ module Graphiti
76
83
  @path ||=
77
84
  path = @sideload.resource.endpoint[:url].to_s
78
85
  if @sideload.type == :belongs_to && !@sideload.remote?
79
- path = "#{path}/#{@model.send(@sideload.foreign_key)}"
86
+ path = "#{path}/#{related_id}"
80
87
  end
81
88
  path
82
89
  end
@@ -102,7 +102,7 @@ class Graphiti::Util::Persistence
102
102
  def apply_derived_attributes(attributes, payload_attributes)
103
103
  return unless @assigned_model
104
104
 
105
- derived = attributes.reject { |key, value| payload_attributes[key] == value }
105
+ derived = @resource.decode_foreign_keys(attributes).reject { |key, value| payload_attributes[key] == value }
106
106
  return if derived.empty?
107
107
 
108
108
  @resource.assign_attributes(@assigned_model, derived, metadata)
@@ -0,0 +1,15 @@
1
+ module Graphiti
2
+ module Util
3
+ class PublicIdBlock
4
+ attr_reader :encoder, :decoder
5
+
6
+ def encode(&blk)
7
+ @encoder = blk
8
+ end
9
+
10
+ def decode(&blk)
11
+ @decoder = blk
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,34 @@
1
+ module Graphiti
2
+ module Util
3
+ class PublicIdMap
4
+ def initialize(resource_class, filter_name)
5
+ @resource_class = resource_class
6
+ @filter_name = filter_name
7
+ @pending = Concurrent::Map.new
8
+ @resolved = {}
9
+ end
10
+
11
+ def add(primary_keys)
12
+ primary_keys.each { |key| @pending[key] = true unless @resolved.key?(key) }
13
+ end
14
+
15
+ def [](primary_key)
16
+ return if primary_key.nil?
17
+
18
+ resolve(@pending.keys) unless @pending.empty?
19
+ resolve([primary_key]) unless @resolved.key?(primary_key)
20
+ @resolved[primary_key]
21
+ end
22
+
23
+ private
24
+
25
+ def resolve(primary_keys)
26
+ found = @resource_class.public_ids_by(@filter_name, primary_keys)
27
+ primary_keys.each do |key|
28
+ @pending.delete(key)
29
+ @resolved[key] = found[key]
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ module Graphiti
2
+ module Util
3
+ class PublicIdSources
4
+ def initialize
5
+ @sources = {}
6
+ end
7
+
8
+ def register(child_resource_class, filter_name, parent_resource_class)
9
+ key = [child_resource_class.name, filter_name.to_sym]
10
+ registered = @sources[key]
11
+ if registered && registered.name != parent_resource_class.name
12
+ raise Errors::ConflictingPublicIdSource.new(child_resource_class, filter_name, registered, parent_resource_class)
13
+ end
14
+ @sources[key] = parent_resource_class
15
+ end
16
+
17
+ def [](child_resource_class, filter_name)
18
+ klass = child_resource_class
19
+ while klass
20
+ source = @sources[[klass.name, filter_name.to_sym]]
21
+ return source if source
22
+ klass = klass.superclass
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -83,10 +83,12 @@ module Graphiti
83
83
  .new(resource_class_ref, sideload_ref, @object)
84
84
  end
85
85
 
86
- unless foreign_key.nil?
86
+ related_id = sideload_ref.rendered_id_for(foreign_key, @proxy.query)
87
+
88
+ unless related_id.nil?
87
89
  {
88
90
  type: sideload_ref.resource.type,
89
- id: foreign_key.to_s
91
+ id: related_id.to_s
90
92
  }
91
93
  end
92
94
  end
@@ -98,7 +100,7 @@ module Graphiti
98
100
  self_ref.send(:validate_link!) unless self_ref.send(:eagerly_validate_links?)
99
101
 
100
102
  link(:related) do
101
- ::Graphiti::Util::Link.new(sideload_ref, @object).generate
103
+ ::Graphiti::Util::Link.new(sideload_ref, @object, @proxy.query).generate
102
104
  end
103
105
  end
104
106
  end