@@ -4,22 +4,121 @@ class JoinTree
4
4
# Stores relationship paths starting from the resource_klass. This allows consolidation of duplicate paths from
5
5
# relationships, filters and sorts. This enables the determination of table aliases as they are joined.
6
6
7
- attr_reader :resource_klass , :options , :source_relationship
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 )
8
15
9
- def initialize ( resource_klass :, options : { } , source_relationship : nil , filters : nil , sort_criteria : nil )
10
16
@resource_klass = resource_klass
11
17
@options = options
12
- @source_relationship = source_relationship
13
-
14
- @join_relationships = { }
15
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 )
16
30
add_sort_criteria ( sort_criteria )
17
31
add_filters ( filters )
32
+ add_relationships ( relationships )
33
+
34
+ @joins = { }
35
+ construct_joins ( @resource_joins )
18
36
end
19
37
20
- # A hash of joins that can be used to create the required joins
21
- def get_joins
22
- walk_relation_node ( @join_relationships )
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
23
122
end
24
123
25
124
def add_filters ( filters )
@@ -41,42 +140,10 @@ def add_sort_criteria(sort_criteria)
41
140
end
42
141
end
43
142
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
-
143
+ def add_relationships ( relationships )
144
+ return if relationships . blank?
61
145
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
146
+ add_join ( relationship , :left )
80
147
end
81
148
end
82
149
@@ -92,35 +159,69 @@ def relation_join_hash(path, path_hash = {})
92
159
end
93
160
94
161
# 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
162
+ # example posts, posts.comments and then posts.comments.author joined in that order will allow each
96
163
# alias to be determined whereas just joining posts.comments.author will only record the author alias.
97
164
# 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 )
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 )
102
179
end
103
180
104
- current_relation_path << value [ :relation_name ] . to_s
105
- current_relationship_path << key . to_s
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
106
189
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
- }
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
+ }
113
199
114
- walk_relation_node ( value [ :relationship ] ,
115
- paths ,
116
- current_relation_path ,
117
- current_relationship_path )
200
+ construct_joins ( resource_details [ :relationships ] ,
201
+ current_relation_path . dup ,
202
+ current_relationship_path . dup )
118
203
119
- current_relation_path . pop
120
- current_relationship_path . pop
204
+ current_relation_path . pop
205
+ current_relationship_path . pop
206
+ end
121
207
end
122
- paths
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 ( '.' )
123
224
end
124
225
end
125
226
end
126
- end
227
+ end
0 commit comments