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

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