Skip to content

Commit 398df59

Browse files
authored
Merge pull request #1191 from cerebris/join_tree
Automatic join aliases, relationship filters, and cleanup
2 parents 18cb4f6 + 3f55ee6 commit 398df59

37 files changed

+2011
-1065
lines changed

.travis.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
language: ruby
22
sudo: false
33
env:
4-
- "RAILS_VERSION=4.2.10"
5-
- "RAILS_VERSION=5.0.7"
6-
- "RAILS_VERSION=5.1.6"
7-
- "RAILS_VERSION=5.2.1"
4+
- "RAILS_VERSION=4.2.11"
5+
- "RAILS_VERSION=5.0.7.1"
6+
- "RAILS_VERSION=5.1.6.1"
7+
- "RAILS_VERSION=5.2.2"
88
- "RAILS_VERSION=master"
99
rvm:
10-
- 2.3.7
11-
- 2.4.4
12-
- 2.5.1
10+
- 2.3.8
11+
- 2.4.5
12+
- 2.5.3
1313
matrix:
1414
allow_failures:
1515
- env: "RAILS_VERSION=master"

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ when 'master'
1919
when 'default'
2020
gem 'railties', '>= 5.0'
2121
else
22+
gem 'left_join' if version.start_with?('4.2')
2223
gem 'railties', "~> #{version}"
2324
end

lib/jsonapi-resources.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,8 @@
2525
require 'jsonapi/callbacks'
2626
require 'jsonapi/link_builder'
2727
require 'jsonapi/active_relation_resource_finder'
28+
require 'jsonapi/active_relation_resource_finder/join_tree'
2829
require 'jsonapi/resource_identity'
30+
require 'jsonapi/resource_fragment'
31+
require 'jsonapi/resource_id_tree'
32+
require 'jsonapi/resource_set'

lib/jsonapi/active_relation_resource_finder.rb

Lines changed: 266 additions & 225 deletions
Large diffs are not rendered by default.
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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
8+
9+
def initialize(resource_klass:, options: {}, source_relationship: nil, filters: nil, sort_criteria: nil)
10+
@resource_klass = resource_klass
11+
@options = options
12+
@source_relationship = source_relationship
13+
14+
@join_relationships = {}
15+
16+
add_sort_criteria(sort_criteria)
17+
add_filters(filters)
18+
end
19+
20+
# A hash of joins that can be used to create the required joins
21+
def get_joins
22+
walk_relation_node(@join_relationships)
23+
end
24+
25+
def add_filters(filters)
26+
return if filters.blank?
27+
filters.each_key do |filter|
28+
# Do not add joins for filters with an apply callable. This can be overridden by setting perform_joins to true
29+
next if resource_klass._allowed_filters[filter].try(:[], :apply) &&
30+
!resource_klass._allowed_filters[filter].try(:[], :perform_joins)
31+
32+
add_join(filter)
33+
end
34+
end
35+
36+
def add_sort_criteria(sort_criteria)
37+
return if sort_criteria.blank?
38+
39+
sort_criteria.each do |sort|
40+
add_join(sort[:field], :left)
41+
end
42+
end
43+
44+
private
45+
46+
def add_join_relationship(parent_joins, join_name, relation_name, type)
47+
parent_joins[join_name] ||= {relation_name: relation_name, relationship: {}, type: type}
48+
if parent_joins[join_name][:type] == :left && type == :inner
49+
parent_joins[join_name][:type] = :inner
50+
end
51+
parent_joins[join_name][:relationship]
52+
end
53+
54+
def add_join(path, default_type = :inner)
55+
relationships, _field = resource_klass.parse_relationship_path(path)
56+
57+
current_joins = @join_relationships
58+
59+
terminated = false
60+
61+
relationships.each do |relationship|
62+
if terminated
63+
# ToDo: Relax this, if possible
64+
# :nocov:
65+
warn "Can not nest joins under polymorphic join"
66+
# :nocov:
67+
end
68+
69+
if relationship.polymorphic?
70+
relation_names = relationship.polymorphic_relations
71+
relation_names.each do |relation_name|
72+
join_name = "#{relationship.name}[#{relation_name}]"
73+
add_join_relationship(current_joins, join_name, relation_name, :left)
74+
end
75+
terminated = true
76+
else
77+
join_name = relationship.name
78+
current_joins = add_join_relationship(current_joins, join_name, relationship.relation_name(options), default_type)
79+
end
80+
end
81+
end
82+
83+
# Create a nested set of hashes from an array of path components. This will be used by the `join` methods.
84+
# [post, comments] => { post: { comments: {} }
85+
def relation_join_hash(path, path_hash = {})
86+
relation = path.shift
87+
if relation
88+
path_hash[relation] = {}
89+
relation_join_hash(path, path_hash[relation])
90+
end
91+
path_hash
92+
end
93+
94+
# Returns the paths from shortest to longest, allowing the capture of the table alias for earlier paths. For
95+
# example posts, posts.comments and then posts.comments.author joined in that order will alow each
96+
# alias to be determined whereas just joining posts.comments.author will only record the author alias.
97+
# ToDo: Dependence on this specialized logic should be removed in the future, if possible.
98+
def walk_relation_node(node, paths = {}, current_relation_path = [], current_relationship_path = [])
99+
node.each do |key, value|
100+
if current_relation_path.empty? && source_relationship
101+
current_relation_path << source_relationship.relation_name(options)
102+
end
103+
104+
current_relation_path << value[:relation_name].to_s
105+
current_relationship_path << key.to_s
106+
107+
rel_path = current_relationship_path.join('.')
108+
paths[rel_path] ||= {
109+
alias: nil,
110+
join_type: value[:type],
111+
relation_join_hash: relation_join_hash(current_relation_path.dup)
112+
}
113+
114+
walk_relation_node(value[:relationship],
115+
paths,
116+
current_relation_path,
117+
current_relationship_path)
118+
119+
current_relation_path.pop
120+
current_relationship_path.pop
121+
end
122+
paths
123+
end
124+
end
125+
end
126+
end

lib/jsonapi/acts_as_resource_controller.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,19 @@ def index_related_resources
6060
end
6161

6262
def get_related_resource
63-
ActiveSupport::Deprecation.warn "In #{self.class.name} you exposed a `get_related_resource`"\
63+
# :nocov:
64+
ActiveSupport::Deprecation.warn "In #{self.class.name} you exposed a `get_related_resource`"\
6465
" action. Please use `show_related_resource` instead."
6566
show_related_resource
67+
# :nocov:
6668
end
6769

6870
def get_related_resources
71+
# :nocov:
6972
ActiveSupport::Deprecation.warn "In #{self.class.name} you exposed a `get_related_resources`"\
7073
" action. Please use `index_related_resource` instead."
7174
index_related_resources
75+
# :nocov:
7276
end
7377

7478
def process_request

lib/jsonapi/exceptions.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module JSONAPI
22
module Exceptions
33
class Error < RuntimeError
4-
attr :error_object_overrides
4+
attr_reader :error_object_overrides
55

66
def initialize(error_object_overrides = {})
77
@error_object_overrides = error_object_overrides

lib/jsonapi/include_directives.rb

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@ class IncludeDirectives
1919
# }
2020
# }
2121

22-
def initialize(resource_klass, includes_array, force_eager_load: false)
22+
def initialize(resource_klass, includes_array)
2323
@resource_klass = resource_klass
24-
@force_eager_load = force_eager_load
2524
@include_directives_hash = { include_related: {} }
2625
includes_array.each do |include|
2726
parse_include(include)
@@ -32,16 +31,6 @@ def include_directives
3231
@include_directives_hash
3332
end
3433

35-
def model_includes
36-
get_includes(@include_directives_hash)
37-
end
38-
39-
# :nocov:
40-
def all_paths
41-
delve_paths(get_includes(@include_directives_hash, false))
42-
end
43-
# :nocov:
44-
4534
private
4635

4736
def get_related(current_path)
@@ -57,24 +46,13 @@ def get_related(current_path)
5746
raise JSONAPI::Exceptions::InvalidInclude.new(current_resource_klass, current_path)
5847
end
5948

60-
include_in_join = @force_eager_load || !current_relationship || current_relationship.eager_load_on_include
6149

62-
current[:include_related][fragment] ||= { include: false, include_related: {}, include_in_join: include_in_join }
50+
current[:include_related][fragment] ||= { include: false, include_related: {} }
6351
current = current[:include_related][fragment]
6452
end
6553
current
6654
end
6755

68-
def get_includes(directive, only_joined_includes = true)
69-
ir = directive[:include_related]
70-
ir = ir.select { |_k,v| v[:include_in_join] } if only_joined_includes
71-
72-
ir.map do |name, sub_directive|
73-
sub = get_includes(sub_directive, only_joined_includes)
74-
sub.any? ? { name => sub } : name
75-
end
76-
end
77-
7856
def parse_include(include)
7957
parts = include.split('.')
8058
local_path = ''
@@ -85,21 +63,5 @@ def parse_include(include)
8563
related[:include] = true
8664
end
8765
end
88-
89-
# :nocov:
90-
def delve_paths(obj)
91-
case obj
92-
when Array
93-
obj.map{|elem| delve_paths(elem)}.flatten(1)
94-
when Hash
95-
obj.map{|k,v| [[k]] + delve_paths(v).map{|path| [k] + path } }.flatten(1)
96-
when Symbol, String
97-
[[obj]]
98-
else
99-
raise "delve_paths cannot descend into #{obj.class.name}"
100-
end
101-
end
102-
# :nocov:
103-
10466
end
10567
end

lib/jsonapi/link_builder.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ def build_engine_name
6161
unless scopes.empty?
6262
"#{ scopes.first.to_s.camelize }::Engine".safe_constantize
6363
end
64+
# :nocov:
6465
rescue LoadError => _e
6566
nil
67+
# :nocov:
6668
end
6769
end
6870

@@ -139,7 +141,9 @@ def regular_primary_resources_url
139141

140142
def regular_resource_path(source)
141143
if source.is_a?(JSONAPI::CachedResponseFragment)
144+
# :nocov:
142145
"#{regular_resources_path(source.resource_klass)}/#{source.id}"
146+
# :nocov:
143147
else
144148
"#{regular_resources_path(source.class)}/#{source.id}"
145149
end

lib/jsonapi/operation_result.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def initialize(code, resource_set, options = {})
4949

5050
def to_hash(serializer)
5151
if serializer
52-
serializer.serialize_resource_set_to_hash(resource_set)
52+
serializer.serialize_resource_set_to_hash_single(resource_set)
5353
else
5454
# :nocov:
5555
{}
@@ -71,7 +71,7 @@ def initialize(code, resource_set, options = {})
7171

7272
def to_hash(serializer)
7373
if serializer
74-
serializer.serialize_resources_set_to_hash(resource_set)
74+
serializer.serialize_resource_set_to_hash_plural(resource_set)
7575
else
7676
# :nocov:
7777
{}
@@ -91,7 +91,7 @@ def initialize(code, source_resource, type, resource_set, options = {})
9191

9292
def to_hash(serializer = nil)
9393
if serializer
94-
serializer.serialize_related_resources_set_to_hash(source_resource, resource_set)
94+
serializer.serialize_related_resource_set_to_hash_plural(resource_set, source_resource)
9595
else
9696
# :nocov:
9797
{}

0 commit comments

Comments
 (0)