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
+ if check_for_duplicate_alias && @collected_aliases . include? ( details [ :alias ] )
127
+ fail "alias '#{ details [ :alias ] } ' has already been added. Possible relation reordering"
128
+ end
129
+
130
+ @collected_aliases << details [ :alias ]
131
+ end
132
+
133
+ def perform_joins ( records , options )
134
+ join_array = flatten_join_tree_by_depth
135
+
136
+ join_array . each do |level_joins |
137
+ level_joins . each do |join_details |
138
+ relationship = join_details [ :relationship ]
139
+ relationship_details = join_details [ :relationship_details ]
140
+ related_resource_klass = join_details [ :related_resource_klass ]
141
+ join_type = relationship_details [ :join_type ]
142
+
143
+ if relationship == :root
144
+ unless source_relationship
145
+ add_join_details ( '' , { alias : resource_klass . _table_name , join_type : :root } )
146
+ end
147
+ next
148
+ end
149
+
150
+ records , join_node = self . class . get_join_arel_node ( records , options ) { |records , options |
151
+ records = related_resource_klass . join_relationship (
152
+ records : records ,
153
+ resource_type : related_resource_klass . _type ,
154
+ join_type : join_type ,
155
+ relationship : relationship ,
156
+ options : options )
157
+ }
158
+
159
+ details = { alias : self . class . alias_from_arel_node ( join_node ) , join_type : join_type }
160
+
161
+ if relationship == source_relationship
162
+ if relationship . polymorphic? && relationship . belongs_to?
163
+ add_join_details ( "##{ related_resource_klass . _type } " , details )
164
+ else
165
+ add_join_details ( '' , details )
166
+ end
167
+ end
168
+
169
+ check_for_duplicate_alias = !( relationship == source_relationship )
170
+ add_join_details ( PathSegment ::Relationship . new ( relationship : relationship , resource_klass : related_resource_klass ) , details , check_for_duplicate_alias )
171
+ end
172
+ end
173
+ records
174
+ end
175
+
176
+ def add_join ( path , default_type = :inner , default_polymorphic_join_type = :left )
177
+ if source_relationship
178
+ if source_relationship . polymorphic?
179
+ # Polymorphic paths will come it with the resource_type as the first segment (for example `#documents.comments`)
180
+ # We just need to prepend the relationship portion the
181
+ sourced_path = "#{ source_relationship . name } #{ path } "
182
+ else
183
+ sourced_path = "#{ source_relationship . name } .#{ path } "
184
+ end
185
+ else
186
+ sourced_path = path
187
+ end
188
+
189
+ join_manager , _field = parse_path_to_tree ( sourced_path , resource_klass , default_type , default_polymorphic_join_type )
190
+
191
+ @resource_join_tree [ :root ] . deep_merge! ( join_manager ) { |key , val , other_val |
192
+ if key == :join_type
193
+ if val == other_val
194
+ val
195
+ else
196
+ :inner
197
+ end
198
+ end
199
+ }
200
+ end
201
+
202
+ def process_path_to_tree ( path_segments , resource_klass , default_join_type , default_polymorphic_join_type )
203
+ node = {
204
+ resource_klasses : {
205
+ resource_klass => {
206
+ relationships : { }
207
+ }
208
+ }
209
+ }
210
+
211
+ segment = path_segments . shift
212
+
213
+ if segment . is_a? ( PathSegment ::Relationship )
214
+ node [ :resource_klasses ] [ resource_klass ] [ :relationships ] [ segment . relationship ] ||= { }
215
+
216
+ # join polymorphic as left joins
217
+ node [ :resource_klasses ] [ resource_klass ] [ :relationships ] [ segment . relationship ] [ :join_type ] ||=
218
+ segment . relationship . polymorphic? ? default_polymorphic_join_type : default_join_type
219
+
220
+ segment . relationship . resource_types . each do |related_resource_type |
221
+ related_resource_klass = resource_klass . resource_klass_for ( related_resource_type )
222
+
223
+ # If the resource type was specified in the path segment we want to only process the next segments for
224
+ # that resource type, otherwise process for all
225
+ process_all_types = !segment . path_specified_resource_klass?
226
+
227
+ if process_all_types || related_resource_klass == segment . resource_klass
228
+ related_resource_tree = process_path_to_tree ( path_segments . dup , related_resource_klass , default_join_type , default_polymorphic_join_type )
229
+ node [ :resource_klasses ] [ resource_klass ] [ :relationships ] [ segment . relationship ] . deep_merge! ( related_resource_tree )
230
+ end
231
+ end
232
+ end
233
+ node
234
+ end
235
+
236
+ def parse_path_to_tree ( path_string , resource_klass , default_join_type = :inner , default_polymorphic_join_type = :left )
237
+ path = JSONAPI ::Path . new ( resource_klass : resource_klass , path_string : path_string )
238
+
239
+ field = path . segments [ -1 ]
240
+ return process_path_to_tree ( path . segments , resource_klass , default_join_type , default_polymorphic_join_type ) , field
241
+ end
242
+
243
+ def add_source_relationship ( source_relationship )
244
+ @source_relationship = source_relationship
245
+
246
+ if @source_relationship
247
+ resource_klasses = { }
248
+ source_relationship . resource_types . each do |related_resource_type |
249
+ related_resource_klass = resource_klass . resource_klass_for ( related_resource_type )
250
+ resource_klasses [ related_resource_klass ] = { relationships : { } }
251
+ end
252
+
253
+ join_type = source_relationship . polymorphic? ? :left : :inner
254
+
255
+ @resource_join_tree [ :root ] [ :resource_klasses ] [ resource_klass ] [ :relationships ] [ @source_relationship ] = {
256
+ source : true , resource_klasses : resource_klasses , join_type : join_type
257
+ }
258
+ end
259
+ end
260
+
261
+ def add_filters ( filters )
262
+ return if filters . blank?
263
+ filters . each_key do |filter |
264
+ # Do not add joins for filters with an apply callable. This can be overridden by setting perform_joins to true
265
+ next if resource_klass . _allowed_filters [ filter ] . try ( :[] , :apply ) &&
266
+ !resource_klass . _allowed_filters [ filter ] . try ( :[] , :perform_joins )
267
+
268
+ add_join ( filter , :left )
269
+ end
270
+ end
271
+
272
+ def add_sort_criteria ( sort_criteria )
273
+ return if sort_criteria . blank?
274
+
275
+ sort_criteria . each do |sort |
276
+ add_join ( sort [ :field ] , :left )
277
+ end
278
+ end
279
+
280
+ def add_relationships ( relationships )
281
+ return if relationships . blank?
282
+ relationships . each do |relationship |
283
+ add_join ( relationship , :left )
284
+ end
285
+ end
286
+ end
287
+ end
288
+ end
0 commit comments