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