Skip to content

Commit 879994e

Browse files
authored
Merge pull request #1112 from senid231/1094-fix-nested-relationship-sorting
Fixes #1094 #1097 fix sorting by nested relationships
2 parents 067b308 + 40cf1eb commit 879994e

5 files changed

Lines changed: 130 additions & 15 deletions

File tree

lib/jsonapi/active_relation_resource_finder.rb

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module ActiveRelationResourceFinder
33
def self.included(base)
44
base.extend ClassMethods
55
end
6-
6+
77
module ClassMethods
88

99
# Finds Resources using the `filters`. Pagination and sort options are used when provided
@@ -387,7 +387,7 @@ def apply_pagination(records, paginator, order_options)
387387
records
388388
end
389389

390-
def apply_sort(records, order_options, context = {})
390+
def apply_sort(records, order_options, _context = {})
391391
if order_options.any?
392392
order_options.each_pair do |field, direction|
393393
if field.to_s.include?(".")
@@ -396,8 +396,7 @@ def apply_sort(records, order_options, context = {})
396396
associations = _lookup_association_chain([records.model.to_s, *model_names])
397397
joins_query = _build_joins([records.model, *associations])
398398

399-
# _sorting is appended to avoid name clashes with manual joins eg. overridden filters
400-
order_by_query = "#{associations.last.name}_sorting.#{column_name} #{direction}"
399+
order_by_query = "#{_join_table_name(associations.last)}.#{column_name} #{direction}"
401400
records = records.joins(joins_query).order(order_by_query)
402401
else
403402
field = _attribute_delegated_name(field)
@@ -423,17 +422,28 @@ def _build_joins(associations)
423422
joins = []
424423

425424
associations.inject do |prev, current|
426-
if current.has_one?
427-
joins << "LEFT JOIN #{current.table_name} AS #{current.name}_sorting ON #{current.name}_sorting.#{current.foreign_key} = #{prev.table_name}.id"
425+
prev_table_name = _join_table_name(prev)
426+
curr_table_name = _join_table_name(current)
427+
if current.belongs_to?
428+
joins << "LEFT JOIN #{current.table_name} AS #{curr_table_name} ON #{curr_table_name}.id = #{prev_table_name}.#{current.foreign_key}"
428429
else
429-
joins << "LEFT JOIN #{current.table_name} AS #{current.name}_sorting ON #{current.name}_sorting.id = #{prev.table_name}.#{current.foreign_key}"
430+
joins << "LEFT JOIN #{current.table_name} AS #{curr_table_name} ON #{curr_table_name}.#{current.foreign_key} = #{prev_table_name}.id"
430431
end
431432

432433
current
433434
end
434435
joins.join("\n")
435436
end
436437

438+
# _sorting is appended to avoid name clashes with manual joins eg. overridden filters
439+
def _join_table_name(association)
440+
if association.is_a?(ActiveRecord::Reflection::AssociationReflection)
441+
"#{association.name}_sorting"
442+
else
443+
association.table_name
444+
end
445+
end
446+
437447
# Assumes ActiveRecord's counting. Override if you need a different counting method
438448
def count_records(records)
439449
records.count(:all)

test/controllers/controller_test.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3911,3 +3911,45 @@ def test_fields_with_delegated_attribute
39113911
JSONAPI.configuration = original_config
39123912
end
39133913
end
3914+
3915+
class WidgetsControllerTest < ActionController::TestCase
3916+
def teardown
3917+
Widget.delete_all
3918+
Indicator.delete_all
3919+
Agency.delete_all
3920+
end
3921+
3922+
def test_fetch_widgets_sort_by_agency_name
3923+
agency_1 = Agency.create! name: 'beta'
3924+
agency_2 = Agency.create! name: 'alpha'
3925+
indicator_1 = Indicator.create! name: 'bar', agency: agency_1
3926+
indicator_2 = Indicator.create! name: 'foo', agency: agency_2
3927+
Widget.create! name: 'bar', indicator: indicator_1
3928+
widget = Widget.create! name: 'foo', indicator: indicator_2
3929+
assert_cacheable_get :index, params: {sort: 'indicator.agency.name'}
3930+
assert_response :success
3931+
assert_equal widget.id.to_s, json_response['data'].first['id']
3932+
end
3933+
end
3934+
3935+
class IndicatorsControllerTest < ActionController::TestCase
3936+
def teardown
3937+
Widget.delete_all
3938+
Indicator.delete_all
3939+
Agency.delete_all
3940+
end
3941+
3942+
def test_fetch_indicators_sort_by_widgets_name
3943+
agency = Agency.create! name: 'test'
3944+
indicator_1 = Indicator.create! name: 'bar', agency: agency
3945+
indicator_2 = Indicator.create! name: 'foo', agency: agency
3946+
Widget.create! name: 'omega', indicator: indicator_1
3947+
Widget.create! name: 'beta', indicator: indicator_1
3948+
Widget.create! name: 'alpha', indicator: indicator_2
3949+
Widget.create! name: 'zeta', indicator: indicator_2
3950+
assert_cacheable_get :index, params: {sort: 'widgets.name'}
3951+
assert_response :success
3952+
assert_equal indicator_2.id.to_s, json_response['data'].first['id']
3953+
assert_equal 2, json_response['data'].size
3954+
end
3955+
end

test/fixtures/active_record.rb

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,23 @@
333333
t.integer :access_card_id, null: false
334334
t.timestamps null: false
335335
end
336+
337+
create_table :agencies, force: true do |t|
338+
t.string :name
339+
t.timestamps null: false
340+
end
341+
342+
create_table :indicators, force: true do |t|
343+
t.string :name
344+
t.integer :agency_id, null: false
345+
t.timestamps null: false
346+
end
347+
348+
create_table :widgets, force: true do |t|
349+
t.string :name
350+
t.integer :indicator_id, null: false
351+
t.timestamps null: false
352+
end
336353
end
337354

338355
### MODELS
@@ -368,7 +385,7 @@ class Post < ActiveRecord::Base
368385
has_many :special_post_tags, source: :tag
369386
has_many :special_tags, through: :special_post_tags, source: :tag
370387
belongs_to :section
371-
has_one :parent_post, class_name: 'Post', foreign_key: 'parent_post_id'
388+
belongs_to :parent_post, class_name: 'Post', foreign_key: 'parent_post_id'
372389

373390
validates :author, presence: true
374391
validates :title, length: { maximum: 35 }
@@ -696,6 +713,18 @@ class Worker < ActiveRecord::Base
696713
belongs_to :access_card
697714
end
698715

716+
class Agency < ActiveRecord::Base
717+
end
718+
719+
class Indicator < ActiveRecord::Base
720+
belongs_to :agency
721+
has_many :widgets
722+
end
723+
724+
class Widget < ActiveRecord::Base
725+
belongs_to :indicator
726+
end
727+
699728
### CONTROLLERS
700729
class AuthorsController < JSONAPI::ResourceControllerMetal
701730
end
@@ -986,6 +1015,12 @@ class AccessCardsController < BaseController
9861015
class WorkersController < BaseController
9871016
end
9881017

1018+
class WidgetsController < JSONAPI::ResourceController
1019+
end
1020+
1021+
class IndicatorsController < JSONAPI::ResourceController
1022+
end
1023+
9891024
### RESOURCES
9901025
class BaseResource < JSONAPI::Resource
9911026
abstract
@@ -1992,6 +2027,29 @@ class BlogPostResource < JSONAPI::Resource
19922027
filter :name
19932028
end
19942029

2030+
class AgencyResource < JSONAPI::Resource
2031+
attributes :name
2032+
end
2033+
2034+
class IndicatorResource < JSONAPI::Resource
2035+
attributes :name
2036+
has_one :agency
2037+
has_many :widgets
2038+
2039+
def self.sortable_fields(_context = nil)
2040+
super + [:'widgets.name']
2041+
end
2042+
end
2043+
2044+
class WidgetResource < JSONAPI::Resource
2045+
attributes :name
2046+
has_one :indicator
2047+
2048+
def self.sortable_fields(_context = nil)
2049+
super + [:'indicator.agency.name']
2050+
end
2051+
end
2052+
19952053
# CustomProcessors
19962054
class Api::V4::BookProcessor < JSONAPI::Processor
19972055
after_find do

test/test_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,8 @@ class CatResource < JSONAPI::Resource
398398
jsonapi_resources :keepers, only: [:show]
399399
jsonapi_resources :storages
400400
jsonapi_resources :workers, only: [:show]
401+
jsonapi_resources :widgets, only: [:index]
402+
jsonapi_resources :indicators, only: [:index]
401403

402404
mount MyEngine::Engine => "/boomshaka", as: :my_engine
403405
mount ApiV2Engine::Engine => "/api_v2", as: :api_v2_engine

test/unit/resource/resource_test.rb

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ def apply_sort(records, order_options, context = {})
345345

346346
def test_lookup_association_chain
347347
model_names = %w(person posts parent_post)
348-
result = PostResource._lookup_association_chain(model_names)
348+
result = PersonResource._lookup_association_chain(model_names)
349349
assert_equal 2, result.length
350350

351351
posts_reflection, parent_post_reflection = result
@@ -361,12 +361,15 @@ def test_lookup_association_chain
361361

362362
def test_build_joins
363363
model_names = %w(person posts parent_post author author_detail)
364-
associations = PostResource._lookup_association_chain(model_names)
365-
result = PostResource.send(:_build_joins, associations)
366-
367-
sql = "LEFT JOIN posts AS parent_post_sorting ON parent_post_sorting.parent_post_id = posts.id
368-
LEFT JOIN people AS author_sorting ON author_sorting.id = posts.author_id
369-
LEFT JOIN author_details AS author_detail_sorting ON author_detail_sorting.person_id = people.id"
364+
associations = PersonResource._lookup_association_chain(model_names)
365+
result = PersonResource.send(:_build_joins, [Person, *associations])
366+
367+
sql = [
368+
'LEFT JOIN posts AS posts_sorting ON posts_sorting.author_id = people.id',
369+
'LEFT JOIN posts AS parent_post_sorting ON parent_post_sorting.id = posts_sorting.parent_post_id',
370+
'LEFT JOIN people AS author_sorting ON author_sorting.id = parent_post_sorting.author_id',
371+
'LEFT JOIN author_details AS author_detail_sorting ON author_detail_sorting.person_id = author_sorting.id'
372+
].join("\n")
370373

371374
assert_equal sql, result
372375
end

0 commit comments

Comments
 (0)