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.
@@ -22,19 +22,27 @@ module JSONAPI
22
22
  end
23
23
 
24
24
  class Relationship
25
- attr_reader :relationship
25
+ attr_reader :relationship, :resource_klass
26
26
 
27
- def initialize(relationship:, resource_klass:)
27
+ def initialize(relationship:, resource_klass: nil)
28
28
  @relationship = relationship
29
29
  @resource_klass = resource_klass
30
30
  end
31
31
 
32
+ def eql?(other)
33
+ relationship == other.relationship && resource_klass == other.resource_klass
34
+ end
35
+
36
+ def hash
37
+ [relationship, resource_klass].hash
38
+ end
39
+
32
40
  def to_s
33
- @resource_klass ? "#{relationship.name}##{resource_klass._type}" : "#{relationship.name}"
41
+ @resource_klass ? "#{relationship.parent_resource_klass._type}.#{relationship.name}##{resource_klass._type}" : "#{resource_klass._type}.#{relationship.name}"
34
42
  end
35
43
 
36
44
  def resource_klass
37
- @resource_klass || @relationship.resource_klass
45
+ @resource_klass || relationship.resource_klass
38
46
  end
39
47
 
40
48
  def path_specified_resource_klass?
@@ -50,13 +58,17 @@ module JSONAPI
50
58
  @field_name = field_name
51
59
  end
52
60
 
61
+ def eql?(other)
62
+ field_name == other.field_name && resource_klass == other.resource_klass
63
+ end
64
+
53
65
  def delegated_field_name
54
66
  resource_klass._attribute_delegated_name(field_name)
55
67
  end
56
68
 
57
69
  def to_s
58
70
  # :nocov:
59
- field_name.to_s
71
+ "#{resource_klass._type}.#{field_name.to_s}"
60
72
  # :nocov:
61
73
  end
62
74
  end
@@ -32,6 +32,7 @@ module JSONAPI
32
32
  end
33
33
 
34
34
  alias_method :polymorphic?, :polymorphic
35
+ alias_method :parent_resource_klass, :parent_resource
35
36
 
36
37
  def primary_key
37
38
  # :nocov:
@@ -5,14 +5,25 @@ module JSONAPI
5
5
 
6
6
  attr_reader :resource_klasses, :populated
7
7
 
8
- def initialize(resource_id_tree)
8
+ def initialize(resource_id_tree = nil)
9
9
  @populated = false
10
- @resource_klasses = flatten_resource_id_tree(resource_id_tree)
10
+ @resource_klasses = resource_id_tree.nil? ? {} : flatten_resource_id_tree(resource_id_tree)
11
11
  end
12
12
 
13
13
  def populate!(serializer, context, find_options)
14
+ # For each resource klass we want to generate the caching key
15
+
16
+ # Hash for collecting types and ids
17
+ # @type [Hash<Class<Resource>, Id[]]]
18
+ missed_resource_ids = {}
19
+
20
+ # Array for collecting CachedResponseFragment::Lookups
21
+ # @type [Lookup[]]
22
+ lookups = []
23
+
24
+
25
+ # Step One collect all of the lookups for the cache, or keys that don't require cache access
14
26
  @resource_klasses.each_key do |resource_klass|
15
- missed_ids = []
16
27
 
17
28
  serializer_config_key = serializer.config_key(resource_klass).gsub("/", "_")
18
29
  context_json = resource_klass.attribute_caching_context(context).to_json
@@ -20,65 +31,124 @@ module JSONAPI
20
31
  context_key = "ATTR-CTX-#{context_b64.gsub("/", "_")}"
21
32
 
22
33
  if resource_klass.caching?
23
- cache_ids = []
24
-
25
- @resource_klasses[resource_klass].each_pair do |k, v|
34
+ cache_ids = @resource_klasses[resource_klass].map do |(k, v)|
26
35
  # Store the hashcode of the cache_field to avoid storing objects and to ensure precision isn't lost
27
36
  # on timestamp types (i.e. string conversions dropping milliseconds)
28
- cache_ids.push([k, resource_klass.hash_cache_field(v[:cache_id])])
37
+ [k, resource_klass.hash_cache_field(v[:cache_id])]
29
38
  end
30
39
 
31
- found_resources = CachedResponseFragment.fetch_cached_fragments(
40
+ lookups.push(
41
+ CachedResponseFragment::Lookup.new(
32
42
  resource_klass,
33
43
  serializer_config_key,
34
- cache_ids,
35
- context)
36
-
37
- found_resources.each do |found_result|
38
- resource = found_result[1]
39
- if resource.nil?
40
- missed_ids.push(found_result[0])
41
- else
42
- @resource_klasses[resource_klass][resource.id][:resource] = resource
43
- end
44
- end
44
+ context,
45
+ context_key,
46
+ cache_ids
47
+ )
48
+ )
45
49
  else
46
- missed_ids = @resource_klasses[resource_klass].keys
50
+ missed_resource_ids[resource_klass] ||= {}
51
+ missed_resource_ids[resource_klass] = @resource_klasses[resource_klass].keys
47
52
  end
53
+ end
54
+
55
+ if lookups.any?
56
+ raise "You've declared some Resources as caching without providing a caching store" if JSONAPI.configuration.resource_cache.nil?
57
+
58
+ # Step Two execute the cache lookup
59
+ found_resources = CachedResponseFragment.lookup(lookups, context)
60
+ else
61
+ found_resources = {}
62
+ end
48
63
 
49
- # fill in any missed resources
50
- unless missed_ids.empty?
51
- find_opts = {
52
- context: context,
53
- fields: find_options[:fields] }
54
-
55
- found_resources = resource_klass.find_by_keys(missed_ids, find_opts)
56
-
57
- found_resources.each do |resource|
58
- relationship_data = @resource_klasses[resource_klass][resource.id][:relationships]
59
-
60
- if resource_klass.caching?
61
- (id, cr) = CachedResponseFragment.write(
62
- resource_klass,
63
- resource,
64
- serializer,
65
- serializer_config_key,
66
- context,
67
- context_key,
68
- relationship_data)
69
-
70
- @resource_klasses[resource_klass][id][:resource] = cr
71
- else
72
- @resource_klasses[resource_klass][resource.id][:resource] = resource
73
- end
64
+
65
+ # Step Three collect the results and collect hit/miss stats
66
+ stats = {}
67
+ found_resources.each do |resource_klass, resources|
68
+ resources.each do |id, cached_resource|
69
+ stats[resource_klass] ||= {}
70
+
71
+ if cached_resource.nil?
72
+ stats[resource_klass][:misses] ||= 0
73
+ stats[resource_klass][:misses] += 1
74
+
75
+ # Collect misses
76
+ missed_resource_ids[resource_klass] ||= []
77
+ missed_resource_ids[resource_klass].push(id)
78
+ else
79
+ stats[resource_klass][:hits] ||= 0
80
+ stats[resource_klass][:hits] += 1
81
+
82
+ register_resource(resource_klass, cached_resource)
74
83
  end
75
84
  end
76
85
  end
77
- @populated = true
86
+
87
+ report_stats(stats)
88
+
89
+ writes = []
90
+
91
+ # Step Four find any of the missing resources and join them into the result
92
+ missed_resource_ids.each_pair do |resource_klass, ids|
93
+ find_opts = {context: context, fields: find_options[:fields]}
94
+ found_resources = resource_klass.find_by_keys(ids, find_opts)
95
+
96
+ found_resources.each do |resource|
97
+ relationship_data = @resource_klasses[resource_klass][resource.id][:relationships]
98
+
99
+ if resource_klass.caching?
100
+
101
+ serializer_config_key = serializer.config_key(resource_klass).gsub("/", "_")
102
+ context_json = resource_klass.attribute_caching_context(context).to_json
103
+ context_b64 = JSONAPI.configuration.resource_cache_digest_function.call(context_json)
104
+ context_key = "ATTR-CTX-#{context_b64.gsub("/", "_")}"
105
+
106
+ writes.push(CachedResponseFragment::Write.new(
107
+ resource_klass,
108
+ resource,
109
+ serializer,
110
+ serializer_config_key,
111
+ context,
112
+ context_key,
113
+ relationship_data
114
+ ))
115
+ end
116
+
117
+ register_resource(resource_klass, resource)
118
+ end
119
+ end
120
+
121
+ # Step Five conditionally write to the cache
122
+ CachedResponseFragment.write(writes) unless JSONAPI.configuration.resource_cache.nil?
123
+
124
+ mark_populated!
78
125
  self
79
126
  end
80
127
 
128
+ def mark_populated!
129
+ @populated = true
130
+ end
131
+
132
+ def register_resource(resource_klass, resource, primary = false)
133
+ @resource_klasses[resource_klass] ||= {}
134
+ @resource_klasses[resource_klass][resource.id] ||= {primary: resource.try(:primary) || primary, relationships: {}}
135
+ @resource_klasses[resource_klass][resource.id][:resource] = resource
136
+ end
137
+
81
138
  private
139
+
140
+ def report_stats(stats)
141
+ return unless JSONAPI.configuration.resource_cache_usage_report_function || JSONAPI.configuration.resource_cache.nil?
142
+
143
+ stats.each_pair do |resource_klass, stat|
144
+ JSONAPI.configuration.resource_cache_usage_report_function.call(
145
+ resource_klass.name,
146
+ stat[:hits] || 0,
147
+ stat[:misses] || 0
148
+ )
149
+ end
150
+ end
151
+
82
152
  def flatten_resource_id_tree(resource_id_tree, flattened_tree = {})
83
153
  resource_id_tree.fragments.each_pair do |resource_rid, fragment|
84
154
 
@@ -87,7 +157,7 @@ module JSONAPI
87
157
 
88
158
  flattened_tree[resource_klass] ||= {}
89
159
 
90
- flattened_tree[resource_klass][id] ||= { primary: fragment.primary, relationships: {} }
160
+ flattened_tree[resource_klass][id] ||= {primary: fragment.primary, relationships: {}}
91
161
  flattened_tree[resource_klass][id][:cache_id] ||= fragment.cache
92
162
 
93
163
  fragment.related.try(:each_pair) do |relationship_name, related_rids|
@@ -104,4 +174,4 @@ module JSONAPI
104
174
  flattened_tree
105
175
  end
106
176
  end
107
- end
177
+ end
@@ -0,0 +1,9 @@
1
+ module JSONAPI
2
+ module Resources
3
+ class Railtie < Rails::Railtie
4
+ rake_tasks do
5
+ load 'tasks/check_upgrade.rake'
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
2
  module Resources
3
- VERSION = '0.10.0.beta2'
3
+ VERSION = '0.10.0.beta2.1'
4
4
  end
5
5
  end
@@ -0,0 +1,52 @@
1
+ require 'rake'
2
+ require 'jsonapi-resources'
3
+
4
+ namespace :jsonapi do
5
+ namespace :resources do
6
+ desc 'Checks application for orphaned overrides'
7
+ task :check_upgrade => :environment do
8
+ Rails.application.eager_load!
9
+
10
+ resource_klasses = ObjectSpace.each_object(Class).select { |klass| klass < JSONAPI::Resource}
11
+
12
+ puts "Checking #{resource_klasses.count} resources"
13
+
14
+ issues_found = 0
15
+
16
+ klasses_with_deprecated = resource_klasses.select { |klass| klass.methods.include?(:find_records) }
17
+ unless klasses_with_deprecated.empty?
18
+ puts " Found the following resources the still implement `find_records`:"
19
+ klasses_with_deprecated.each { |klass| puts " #{klass}"}
20
+ puts " The `find_records` method is no longer called by JR. Please review and ensure your functionality is ported over."
21
+
22
+ issues_found = issues_found + klasses_with_deprecated.length
23
+ end
24
+
25
+ klasses_with_deprecated = resource_klasses.select { |klass| klass.methods.include?(:records_for) }
26
+ unless klasses_with_deprecated.empty?
27
+ puts " Found the following resources the still implement `records_for`:"
28
+ klasses_with_deprecated.each { |klass| puts " #{klass}"}
29
+ puts " The `records_for` method is no longer called by JR. Please review and ensure your functionality is ported over."
30
+
31
+ issues_found = issues_found + klasses_with_deprecated.length
32
+ end
33
+
34
+ klasses_with_deprecated = resource_klasses.select { |klass| klass.methods.include?(:apply_includes) }
35
+ unless klasses_with_deprecated.empty?
36
+ puts " Found the following resources the still implement `apply_includes`:"
37
+ klasses_with_deprecated.each { |klass| puts " #{klass}"}
38
+ puts " The `apply_includes` method is no longer called by JR. Please review and ensure your functionality is ported over."
39
+
40
+ issues_found = issues_found + klasses_with_deprecated.length
41
+ end
42
+
43
+ if issues_found > 0
44
+ puts "Finished inspection. #{issues_found} issues found that may impact upgrading. Please address these issues. "
45
+ else
46
+ puts "Finished inspection with no issues found. Note this is only a cursory check for method overrides that will no \n" \
47
+ "longer be called by JSONAPI::Resources. This check in no way assures your code will continue to function as \n" \
48
+ "it did before the upgrade. Please do adequate testing before using in production."
49
+ end
50
+ end
51
+ end
52
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsonapi-resources
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0.beta2
4
+ version: 0.10.0.beta2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Gebhardt
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-02-11 00:00:00.000000000 Z
12
+ date: 2019-02-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -178,7 +178,7 @@ files:
178
178
  - lib/jsonapi-resources.rb
179
179
  - lib/jsonapi/active_relation_resource_finder.rb
180
180
  - lib/jsonapi/active_relation_resource_finder/adapters/join_left_active_record_adapter.rb
181
- - lib/jsonapi/active_relation_resource_finder/join_tree.rb
181
+ - lib/jsonapi/active_relation_resource_finder/join_manager.rb
182
182
  - lib/jsonapi/acts_as_resource_controller.rb
183
183
  - lib/jsonapi/cached_response_fragment.rb
184
184
  - lib/jsonapi/callbacks.rb
@@ -208,9 +208,11 @@ files:
208
208
  - lib/jsonapi/resource_identity.rb
209
209
  - lib/jsonapi/resource_serializer.rb
210
210
  - lib/jsonapi/resource_set.rb
211
+ - lib/jsonapi/resources/railtie.rb
211
212
  - lib/jsonapi/resources/version.rb
212
213
  - lib/jsonapi/response_document.rb
213
214
  - lib/jsonapi/routing_ext.rb
215
+ - lib/tasks/check_upgrade.rake
214
216
  homepage: https://github.com/cerebris/jsonapi-resources
215
217
  licenses:
216
218
  - MIT
@@ -230,8 +232,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
230
232
  - !ruby/object:Gem::Version
231
233
  version: 1.3.1
232
234
  requirements: []
233
- rubyforge_project:
234
- rubygems_version: 2.6.14.3
235
+ rubygems_version: 3.0.1
235
236
  signing_key:
236
237
  specification_version: 4
237
238
  summary: Easily support JSON API in Rails.
@@ -1,227 +0,0 @@
1
- module JSONAPI
2
- module ActiveRelationResourceFinder
3
- class JoinTree
4
- # Stores relationship paths starting from the resource_klass. This allows consolidation of duplicate paths from
5
- # relationships, filters and sorts. This enables the determination of table aliases as they are joined.
6
-
7
- attr_reader :resource_klass, :options, :source_relationship, :resource_joins, :joins
8
-
9
- def initialize(resource_klass:,
10
- options: {},
11
- source_relationship: nil,
12
- relationships: nil,
13
- filters: nil,
14
- sort_criteria: nil)
15
-
16
- @resource_klass = resource_klass
17
- @options = options
18
-
19
- @resource_joins = {
20
- root: {
21
- join_type: :root,
22
- resource_klasses: {
23
- resource_klass => {
24
- relationships: {}
25
- }
26
- }
27
- }
28
- }
29
- add_source_relationship(source_relationship)
30
- add_sort_criteria(sort_criteria)
31
- add_filters(filters)
32
- add_relationships(relationships)
33
-
34
- @joins = {}
35
- construct_joins(@resource_joins)
36
- end
37
-
38
- private
39
-
40
- def add_join(path, default_type = :inner, default_polymorphic_join_type = :left)
41
- if source_relationship
42
- if source_relationship.polymorphic?
43
- # Polymorphic paths will come it with the resource_type as the first segment (for example `#documents.comments`)
44
- # We just need to prepend the relationship portion the
45
- sourced_path = "#{source_relationship.name}#{path}"
46
- else
47
- sourced_path = "#{source_relationship.name}.#{path}"
48
- end
49
- else
50
- sourced_path = path
51
- end
52
-
53
- join_tree, _field = parse_path_to_tree(sourced_path, resource_klass, default_type, default_polymorphic_join_type)
54
-
55
- @resource_joins[:root].deep_merge!(join_tree) { |key, val, other_val|
56
- if key == :join_type
57
- if val == other_val
58
- val
59
- else
60
- :inner
61
- end
62
- end
63
- }
64
- end
65
-
66
- def process_path_to_tree(path_segments, resource_klass, default_join_type, default_polymorphic_join_type)
67
- node = {
68
- resource_klasses: {
69
- resource_klass => {
70
- relationships: {}
71
- }
72
- }
73
- }
74
-
75
- segment = path_segments.shift
76
-
77
- if segment.is_a?(PathSegment::Relationship)
78
- node[:resource_klasses][resource_klass][:relationships][segment.relationship] ||= {}
79
-
80
- # join polymorphic as left joins
81
- node[:resource_klasses][resource_klass][:relationships][segment.relationship][:join_type] ||=
82
- segment.relationship.polymorphic? ? default_polymorphic_join_type : default_join_type
83
-
84
- segment.relationship.resource_types.each do |related_resource_type|
85
- related_resource_klass = resource_klass.resource_klass_for(related_resource_type)
86
-
87
- # If the resource type was specified in the path segment we want to only process the next segments for
88
- # that resource type, otherwise process for all
89
- process_all_types = !segment.path_specified_resource_klass?
90
-
91
- if process_all_types || related_resource_klass == segment.resource_klass
92
- related_resource_tree = process_path_to_tree(path_segments.dup, related_resource_klass, default_join_type, default_polymorphic_join_type)
93
- node[:resource_klasses][resource_klass][:relationships][segment.relationship].deep_merge!(related_resource_tree)
94
- end
95
- end
96
- end
97
- node
98
- end
99
-
100
- def parse_path_to_tree(path_string, resource_klass, default_join_type = :inner, default_polymorphic_join_type = :left)
101
- path = JSONAPI::Path.new(resource_klass: resource_klass, path_string: path_string)
102
- field = path.segments[-1]
103
- return process_path_to_tree(path.segments, resource_klass, default_join_type, default_polymorphic_join_type), field
104
- end
105
-
106
- def add_source_relationship(source_relationship)
107
- @source_relationship = source_relationship
108
-
109
- if @source_relationship
110
- resource_klasses = {}
111
- source_relationship.resource_types.each do |related_resource_type|
112
- related_resource_klass = resource_klass.resource_klass_for(related_resource_type)
113
- resource_klasses[related_resource_klass] = {relationships: {}}
114
- end
115
-
116
- join_type = source_relationship.polymorphic? ? :left : :inner
117
-
118
- @resource_joins[:root][:resource_klasses][resource_klass][:relationships][@source_relationship] = {
119
- source: true, resource_klasses: resource_klasses, join_type: join_type
120
- }
121
- end
122
- end
123
-
124
- def add_filters(filters)
125
- return if filters.blank?
126
- filters.each_key do |filter|
127
- # Do not add joins for filters with an apply callable. This can be overridden by setting perform_joins to true
128
- next if resource_klass._allowed_filters[filter].try(:[], :apply) &&
129
- !resource_klass._allowed_filters[filter].try(:[], :perform_joins)
130
-
131
- add_join(filter)
132
- end
133
- end
134
-
135
- def add_sort_criteria(sort_criteria)
136
- return if sort_criteria.blank?
137
-
138
- sort_criteria.each do |sort|
139
- add_join(sort[:field], :left)
140
- end
141
- end
142
-
143
- def add_relationships(relationships)
144
- return if relationships.blank?
145
- relationships.each do |relationship|
146
- add_join(relationship, :left)
147
- end
148
- end
149
-
150
- # Create a nested set of hashes from an array of path components. This will be used by the `join` methods.
151
- # [post, comments] => { post: { comments: {} }
152
- def relation_join_hash(path, path_hash = {})
153
- relation = path.shift
154
- if relation
155
- path_hash[relation] = {}
156
- relation_join_hash(path, path_hash[relation])
157
- end
158
- path_hash
159
- end
160
-
161
- # Returns the paths from shortest to longest, allowing the capture of the table alias for earlier paths. For
162
- # example posts, posts.comments and then posts.comments.author joined in that order will allow each
163
- # alias to be determined whereas just joining posts.comments.author will only record the author alias.
164
- # ToDo: Dependence on this specialized logic should be removed in the future, if possible.
165
- def construct_joins(node, current_relation_path = [], current_relationship_path = [])
166
- node.each do |relationship, relationship_details|
167
- join_type = relationship_details[:join_type]
168
- if relationship == :root
169
- @joins[:root] = {alias: resource_klass._table_name, join_type: :root}
170
-
171
- # alias to the default table unless a source_relationship is specified
172
- unless source_relationship
173
- @joins[''] = {alias: resource_klass._table_name, join_type: :root}
174
- end
175
-
176
- return construct_joins(relationship_details[:resource_klasses].values[0][:relationships],
177
- current_relation_path,
178
- current_relationship_path)
179
- end
180
-
181
- relationship_details[:resource_klasses].each do |resource_klass, resource_details|
182
- if relationship.polymorphic? && relationship.belongs_to?
183
- current_relationship_path << "#{relationship.name.to_s}##{resource_klass._type.to_s}"
184
- relation_name = resource_klass._type.to_s.singularize
185
- else
186
- current_relationship_path << relationship.name.to_s
187
- relation_name = relationship.relation_name(options).to_s
188
- end
189
-
190
- current_relation_path << relation_name
191
-
192
- rel_path = calc_path_string(current_relationship_path)
193
-
194
- @joins[rel_path] = {
195
- alias: nil,
196
- join_type: join_type,
197
- relation_join_hash: relation_join_hash(current_relation_path.dup)
198
- }
199
-
200
- construct_joins(resource_details[:relationships],
201
- current_relation_path.dup,
202
- current_relationship_path.dup)
203
-
204
- current_relation_path.pop
205
- current_relationship_path.pop
206
- end
207
- end
208
- end
209
-
210
- def calc_path_string(path_array)
211
- if source_relationship
212
- if source_relationship.polymorphic?
213
- _relationship_name, resource_name = path_array[0].split('#', 2)
214
- path = path_array.dup
215
- path[0] = "##{resource_name}"
216
- else
217
- path = path_array.dup.drop(1)
218
- end
219
- else
220
- path = path_array.dup
221
- end
222
-
223
- path.join('.')
224
- end
225
- end
226
- end
227
- end