jsonapi-resources 0.10.0.beta2 → 0.10.0.beta2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,297 @@
1
+ module JSONAPI
2
+ module ActiveRelationResourceFinder
3
+
4
+ # Stores relationship paths starting from the resource_klass, consolidating duplicate paths from
5
+ # relationships, filters and sorts. When joins are made the table aliases are tracked in join_details
6
+ class JoinManager
7
+ attr_reader :resource_klass,
8
+ :source_relationship,
9
+ :resource_join_tree,
10
+ :join_details
11
+
12
+ def initialize(resource_klass:,
13
+ source_relationship: nil,
14
+ relationships: nil,
15
+ filters: nil,
16
+ sort_criteria: nil)
17
+
18
+ @resource_klass = resource_klass
19
+ @join_details = nil
20
+ @collected_aliases = Set.new
21
+
22
+ @resource_join_tree = {
23
+ root: {
24
+ join_type: :root,
25
+ resource_klasses: {
26
+ resource_klass => {
27
+ relationships: {}
28
+ }
29
+ }
30
+ }
31
+ }
32
+ add_source_relationship(source_relationship)
33
+ add_sort_criteria(sort_criteria)
34
+ add_filters(filters)
35
+ add_relationships(relationships)
36
+ end
37
+
38
+ def join(records, options)
39
+ fail "can't be joined again" if @join_details
40
+ @join_details = {}
41
+ perform_joins(records, options)
42
+ end
43
+
44
+ # source details will only be on a relationship if the source_relationship is set
45
+ # this method gets the join details whether they are on a relationship or are just pseudo details for the base
46
+ # resource. Specify the resource type for polymorphic relationships
47
+ #
48
+ def source_join_details(type=nil)
49
+ if source_relationship
50
+ related_resource_klass = type ? resource_klass.resource_klass_for(type) : source_relationship.resource_klass
51
+ segment = PathSegment::Relationship.new(relationship: source_relationship, resource_klass: related_resource_klass)
52
+ details = @join_details[segment]
53
+ else
54
+ if type
55
+ details = @join_details["##{type}"]
56
+ else
57
+ details = @join_details['']
58
+ end
59
+ end
60
+ details
61
+ end
62
+
63
+ def join_details_by_polymorphic_relationship(relationship, type)
64
+ segment = PathSegment::Relationship.new(relationship: relationship, resource_klass: resource_klass.resource_klass_for(type))
65
+ @join_details[segment]
66
+ end
67
+
68
+ def join_details_by_relationship(relationship)
69
+ segment = PathSegment::Relationship.new(relationship: relationship, resource_klass: relationship.resource_klass)
70
+ @join_details[segment]
71
+ end
72
+
73
+ def self.get_join_arel_node(records, options = {})
74
+ init_join_sources = records.arel.join_sources
75
+ init_join_sources_length = init_join_sources.length
76
+
77
+ records = yield(records, options)
78
+
79
+ join_sources = records.arel.join_sources
80
+ if join_sources.length > init_join_sources_length
81
+ last_join = (join_sources - init_join_sources).last
82
+ else
83
+ # :nocov:
84
+ warn "get_join_arel_node: No join added"
85
+ last_join = nil
86
+ # :nocov:
87
+ end
88
+
89
+ return records, last_join
90
+ end
91
+
92
+ def self.alias_from_arel_node(node)
93
+ case node.left
94
+ when Arel::Table
95
+ node.left.name
96
+ when Arel::Nodes::TableAlias
97
+ node.left.right
98
+ when Arel::Nodes::StringJoin
99
+ # :nocov:
100
+ warn "alias_from_arel_node: Unsupported join type - use custom filtering and sorting"
101
+ nil
102
+ # :nocov:
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def flatten_join_tree_by_depth(join_array = [], node = @resource_join_tree, level = 0)
109
+ join_array[level] = [] unless join_array[level]
110
+
111
+ node.each do |relationship, relationship_details|
112
+ relationship_details[:resource_klasses].each do |related_resource_klass, resource_details|
113
+ join_array[level] << { relationship: relationship,
114
+ relationship_details: relationship_details,
115
+ related_resource_klass: related_resource_klass}
116
+ flatten_join_tree_by_depth(join_array, resource_details[:relationships], level+1)
117
+ end
118
+ end
119
+ join_array
120
+ end
121
+
122
+ def add_join_details(join_key, details, check_for_duplicate_alias = true)
123
+ fail "details already set" if @join_details.has_key?(join_key)
124
+ @join_details[join_key] = details
125
+
126
+ # Joins are being tracked as they are added to the built up relation. If the same table is added to a
127
+ # relation more than once subsequent versions will be assigned an alias. Depending on the order the joins
128
+ # are made the computed aliases may change. The order this library performs the joins was chosen
129
+ # to prevent this. However if the relation is reordered it should result in reusing on of the earlier
130
+ # aliases (in this case a plain table name). The following check will catch this an raise an exception.
131
+ # An exception is appropriate because not using the correct alias could leak data due to filters and
132
+ # applied permissions being performed on the wrong data.
133
+ if check_for_duplicate_alias && @collected_aliases.include?(details[:alias])
134
+ fail "alias '#{details[:alias]}' has already been added. Possible relation reordering"
135
+ end
136
+
137
+ @collected_aliases << details[:alias]
138
+ end
139
+
140
+ def perform_joins(records, options)
141
+ join_array = flatten_join_tree_by_depth
142
+
143
+ join_array.each do |level_joins|
144
+ level_joins.each do |join_details|
145
+ relationship = join_details[:relationship]
146
+ relationship_details = join_details[:relationship_details]
147
+ related_resource_klass = join_details[:related_resource_klass]
148
+ join_type = relationship_details[:join_type]
149
+
150
+ if relationship == :root
151
+ unless source_relationship
152
+ add_join_details('', {alias: resource_klass._table_name, join_type: :root})
153
+ end
154
+ next
155
+ end
156
+
157
+ records, join_node = self.class.get_join_arel_node(records, options) {|records, options|
158
+ records = related_resource_klass.join_relationship(
159
+ records: records,
160
+ resource_type: related_resource_klass._type,
161
+ join_type: join_type,
162
+ relationship: relationship,
163
+ options: options)
164
+ }
165
+
166
+ details = {alias: self.class.alias_from_arel_node(join_node), join_type: join_type}
167
+
168
+ if relationship == source_relationship
169
+ if relationship.polymorphic? && relationship.belongs_to?
170
+ add_join_details("##{related_resource_klass._type}", details)
171
+ else
172
+ add_join_details('', details)
173
+ end
174
+ end
175
+
176
+ # We're adding the source alias with two keys. We only want the check for duplicate aliases once.
177
+ # See the note in `add_join_details`.
178
+ check_for_duplicate_alias = !(relationship == source_relationship)
179
+ add_join_details(PathSegment::Relationship.new(relationship: relationship, resource_klass: related_resource_klass), details, check_for_duplicate_alias)
180
+ end
181
+ end
182
+ records
183
+ end
184
+
185
+ def add_join(path, default_type = :inner, default_polymorphic_join_type = :left)
186
+ if source_relationship
187
+ if source_relationship.polymorphic?
188
+ # Polymorphic paths will come it with the resource_type as the first segment (for example `#documents.comments`)
189
+ # We just need to prepend the relationship portion the
190
+ sourced_path = "#{source_relationship.name}#{path}"
191
+ else
192
+ sourced_path = "#{source_relationship.name}.#{path}"
193
+ end
194
+ else
195
+ sourced_path = path
196
+ end
197
+
198
+ join_manager, _field = parse_path_to_tree(sourced_path, resource_klass, default_type, default_polymorphic_join_type)
199
+
200
+ @resource_join_tree[:root].deep_merge!(join_manager) { |key, val, other_val|
201
+ if key == :join_type
202
+ if val == other_val
203
+ val
204
+ else
205
+ :inner
206
+ end
207
+ end
208
+ }
209
+ end
210
+
211
+ def process_path_to_tree(path_segments, resource_klass, default_join_type, default_polymorphic_join_type)
212
+ node = {
213
+ resource_klasses: {
214
+ resource_klass => {
215
+ relationships: {}
216
+ }
217
+ }
218
+ }
219
+
220
+ segment = path_segments.shift
221
+
222
+ if segment.is_a?(PathSegment::Relationship)
223
+ node[:resource_klasses][resource_klass][:relationships][segment.relationship] ||= {}
224
+
225
+ # join polymorphic as left joins
226
+ node[:resource_klasses][resource_klass][:relationships][segment.relationship][:join_type] ||=
227
+ segment.relationship.polymorphic? ? default_polymorphic_join_type : default_join_type
228
+
229
+ segment.relationship.resource_types.each do |related_resource_type|
230
+ related_resource_klass = resource_klass.resource_klass_for(related_resource_type)
231
+
232
+ # If the resource type was specified in the path segment we want to only process the next segments for
233
+ # that resource type, otherwise process for all
234
+ process_all_types = !segment.path_specified_resource_klass?
235
+
236
+ if process_all_types || related_resource_klass == segment.resource_klass
237
+ related_resource_tree = process_path_to_tree(path_segments.dup, related_resource_klass, default_join_type, default_polymorphic_join_type)
238
+ node[:resource_klasses][resource_klass][:relationships][segment.relationship].deep_merge!(related_resource_tree)
239
+ end
240
+ end
241
+ end
242
+ node
243
+ end
244
+
245
+ def parse_path_to_tree(path_string, resource_klass, default_join_type = :inner, default_polymorphic_join_type = :left)
246
+ path = JSONAPI::Path.new(resource_klass: resource_klass, path_string: path_string)
247
+
248
+ field = path.segments[-1]
249
+ return process_path_to_tree(path.segments, resource_klass, default_join_type, default_polymorphic_join_type), field
250
+ end
251
+
252
+ def add_source_relationship(source_relationship)
253
+ @source_relationship = source_relationship
254
+
255
+ if @source_relationship
256
+ resource_klasses = {}
257
+ source_relationship.resource_types.each do |related_resource_type|
258
+ related_resource_klass = resource_klass.resource_klass_for(related_resource_type)
259
+ resource_klasses[related_resource_klass] = {relationships: {}}
260
+ end
261
+
262
+ join_type = source_relationship.polymorphic? ? :left : :inner
263
+
264
+ @resource_join_tree[:root][:resource_klasses][resource_klass][:relationships][@source_relationship] = {
265
+ source: true, resource_klasses: resource_klasses, join_type: join_type
266
+ }
267
+ end
268
+ end
269
+
270
+ def add_filters(filters)
271
+ return if filters.blank?
272
+ filters.each_key do |filter|
273
+ # Do not add joins for filters with an apply callable. This can be overridden by setting perform_joins to true
274
+ next if resource_klass._allowed_filters[filter].try(:[], :apply) &&
275
+ !resource_klass._allowed_filters[filter].try(:[], :perform_joins)
276
+
277
+ add_join(filter, :left)
278
+ end
279
+ end
280
+
281
+ def add_sort_criteria(sort_criteria)
282
+ return if sort_criteria.blank?
283
+
284
+ sort_criteria.each do |sort|
285
+ add_join(sort[:field], :left)
286
+ end
287
+ end
288
+
289
+ def add_relationships(relationships)
290
+ return if relationships.blank?
291
+ relationships.each do |relationship|
292
+ add_join(relationship, :left)
293
+ end
294
+ end
295
+ end
296
+ end
297
+ end
@@ -1,22 +1,42 @@
1
1
  module JSONAPI
2
2
  class CachedResponseFragment
3
- def self.fetch_cached_fragments(resource_klass, serializer_config_key, cache_ids, context)
4
- context_json = resource_klass.attribute_caching_context(context).to_json
5
- context_b64 = JSONAPI.configuration.resource_cache_digest_function.call(context_json)
6
- context_key = "ATTR-CTX-#{context_b64.gsub("/", "_")}"
7
-
8
- results = self.lookup(resource_klass, serializer_config_key, context, context_key, cache_ids)
9
-
10
- if JSONAPI.configuration.resource_cache_usage_report_function
11
- miss_ids = results.select{|_k,v| v.nil? }.keys
12
- JSONAPI.configuration.resource_cache_usage_report_function.call(
13
- resource_klass.name,
14
- cache_ids.size - miss_ids.size,
15
- miss_ids.size
16
- )
3
+
4
+ Lookup = Struct.new(:resource_klass, :serializer_config_key, :context, :context_key, :cache_ids) do
5
+
6
+ def type
7
+ resource_klass._type
17
8
  end
18
9
 
19
- results
10
+ def keys
11
+ cache_ids.map do |(id, cache_key)|
12
+ [type, id, cache_key, serializer_config_key, context_key]
13
+ end
14
+ end
15
+ end
16
+
17
+ Write = Struct.new(:resource_klass, :resource, :serializer, :serializer_config_key, :context, :context_key, :relationship_data) do
18
+ def to_key_value
19
+
20
+ (id, cache_key) = resource.cache_id
21
+
22
+ json = serializer.object_hash(resource, relationship_data)
23
+
24
+ cr = CachedResponseFragment.new(
25
+ resource_klass,
26
+ id,
27
+ json['type'],
28
+ context,
29
+ resource.fetchable_fields,
30
+ json['relationships'],
31
+ json['links'],
32
+ json['attributes'],
33
+ json['meta']
34
+ )
35
+
36
+ key = [resource_klass._type, id, cache_key, serializer_config_key, context_key]
37
+
38
+ [key, cr]
39
+ end
20
40
  end
21
41
 
22
42
  attr_reader :resource_klass, :id, :type, :context, :fetchable_fields, :relationships,
@@ -50,26 +70,46 @@ module JSONAPI
50
70
  }
51
71
  end
52
72
 
53
- private
73
+ # @param [Lookup[]] lookups
74
+ # @return [Hash<Class<Resource>, Hash<ID, CachedResourceFragment>>]
75
+ def self.lookup(lookups, context)
76
+ type_to_klass = lookups.map {|l| [l.type, l.resource_klass]}.to_h
54
77
 
55
- def self.lookup(resource_klass, serializer_config_key, context, context_key, cache_ids)
56
- type = resource_klass._type
78
+ keys = lookups.map(&:keys).flatten(1)
57
79
 
58
- keys = cache_ids.map do |(id, cache_key)|
59
- [type, id, cache_key, serializer_config_key, context_key]
60
- end
80
+ hits = JSONAPI.configuration.resource_cache.read_multi(*keys).reject {|_, v| v.nil?}
81
+
82
+ return keys.inject({}) do |hash, key|
83
+ (type, id, _, _) = key
84
+ resource_klass = type_to_klass[type]
85
+ hash[resource_klass] ||= {}
61
86
 
62
- hits = JSONAPI.configuration.resource_cache.read_multi(*keys).reject{|_,v| v.nil? }
63
- return keys.each_with_object({}) do |key, hash|
64
- (_, id, _, _) = key
65
87
  if hits.has_key?(key)
66
- hash[id] = self.from_cache_value(resource_klass, context, hits[key])
88
+ hash[resource_klass][id] = self.from_cache_value(resource_klass, context, hits[key])
67
89
  else
68
- hash[id] = nil
90
+ hash[resource_klass][id] = nil
69
91
  end
92
+
93
+ hash
70
94
  end
71
95
  end
72
96
 
97
+ # @param [Write[]] lookups
98
+ def self.write(writes)
99
+ key_values = writes.map(&:to_key_value)
100
+
101
+ to_write = key_values.map {|(k, v)| [k, v.to_cache_value]}.to_h
102
+
103
+ if JSONAPI.configuration.resource_cache.respond_to? :write_multi
104
+ JSONAPI.configuration.resource_cache.write_multi(to_write)
105
+ else
106
+ to_write.each do |key, value|
107
+ JSONAPI.configuration.resource_cache.write(key, value)
108
+ end
109
+ end
110
+
111
+ end
112
+
73
113
  def self.from_cache_value(resource_klass, context, h)
74
114
  new(
75
115
  resource_klass,
@@ -83,28 +123,5 @@ module JSONAPI
83
123
  h.fetch(:meta, nil)
84
124
  )
85
125
  end
86
-
87
- def self.write(resource_klass, resource, serializer, serializer_config_key, context, context_key, relationship_data )
88
- (id, cache_key) = resource.cache_id
89
-
90
- json = serializer.object_hash(resource, relationship_data)
91
-
92
- cr = self.new(
93
- resource_klass,
94
- id,
95
- json['type'],
96
- context,
97
- resource.fetchable_fields,
98
- json['relationships'],
99
- json['links'],
100
- json['attributes'],
101
- json['meta']
102
- )
103
-
104
- key = [resource_klass._type, id, cache_key, serializer_config_key, context_key]
105
- JSONAPI.configuration.resource_cache.write(key, cr.to_cache_value)
106
- return [id, cr]
107
- end
108
-
109
126
  end
110
127
  end
data/lib/jsonapi/path.rb CHANGED
@@ -31,5 +31,13 @@ module JSONAPI
31
31
  def relationship_path_string
32
32
  relationship_segments.collect(&:to_s).join('.')
33
33
  end
34
+
35
+ def last_relationship
36
+ if @segments.last.is_a?(PathSegment::Relationship)
37
+ @segments.last
38
+ else
39
+ @segments[-2]
40
+ end
41
+ end
34
42
  end
35
43
  end