Skip to content

Commit 5dfeb0f

Browse files
authored
Merge pull request #1109 from senid231/936-custom-sorting
Fixes #936 add ability to define custom sorting
2 parents 33d0cb7 + 7a05c2b commit 5dfeb0f

File tree

5 files changed

+114
-26
lines changed

5 files changed

+114
-26
lines changed

lib/jsonapi/active_relation_resource_finder.rb

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -387,28 +387,38 @@ 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|
393-
if field.to_s.include?(".")
394-
*model_names, column_name = field.split(".")
395-
396-
associations = _lookup_association_chain([records.model.to_s, *model_names])
397-
joins_query = _build_joins([records.model, *associations])
398-
399-
order_by_query = "#{_join_table_name(associations.last)}.#{column_name} #{direction}"
400-
records = records.joins(joins_query).order(order_by_query)
401-
else
402-
field = _attribute_delegated_name(field)
403-
records = records.order(field => direction)
404-
end
393+
records = apply_single_sort(records, field, direction, context)
405394
end
406395
end
407396

408397
records
409398
end
410399

411-
def apply_basic_sort(records, order_options, context = {})
400+
def apply_single_sort(records, field, direction, context = {})
401+
strategy = _allowed_sort.fetch(field.to_sym, {})[:apply]
402+
403+
if strategy
404+
call_method_or_proc(strategy, records, direction, context)
405+
else
406+
if field.to_s.include?(".")
407+
*model_names, column_name = field.split(".")
408+
409+
associations = _lookup_association_chain([records.model.to_s, *model_names])
410+
joins_query = _build_joins([records.model, *associations])
411+
412+
order_by_query = "#{_join_table_name(associations.last)}.#{column_name} #{direction}"
413+
records.joins(joins_query).order(order_by_query)
414+
else
415+
field = _attribute_delegated_name(field)
416+
records.order(field => direction)
417+
end
418+
end
419+
end
420+
421+
def apply_basic_sort(records, order_options, _context = {})
412422
if order_options.any?
413423
order_options.each_pair do |field, direction|
414424
records = records.order("#{field} #{direction}")
@@ -476,11 +486,7 @@ def apply_filter(records, filter, value, options = {})
476486
strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]
477487

478488
if strategy
479-
if strategy.is_a?(Symbol) || strategy.is_a?(String)
480-
send(strategy, records, value, options)
481-
else
482-
strategy.call(records, value, options)
483-
end
489+
call_method_or_proc(strategy, records, value, options)
484490
else
485491
filter = _attribute_delegated_name(filter)
486492
table_alias = options[:table_alias]

lib/jsonapi/resource.rb

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,8 @@ def inherited(subclass)
416416

417417
subclass._allowed_filters = (_allowed_filters || Set.new).dup
418418

419+
subclass._allowed_sort = _allowed_sort.dup
420+
419421
type = subclass.name.demodulize.sub(/Resource$/, '').underscore
420422
subclass._type = type.pluralize.to_sym
421423

@@ -537,7 +539,7 @@ def model_name_for_type(key_type)
537539
end
538540

539541
attr_accessor :_attributes, :_relationships, :_type, :_model_hints
540-
attr_writer :_allowed_filters, :_paginator
542+
attr_writer :_allowed_filters, :_paginator, :_allowed_sort
541543

542544
def create(context)
543545
new(create_model, context)
@@ -583,6 +585,10 @@ def attribute(attribute_name, options = {})
583585
define_method "#{attr}=" do |value|
584586
@model.public_send("#{options[:delegate] ? options[:delegate].to_sym : attr}=", value)
585587
end unless method_defined?("#{attr}=")
588+
589+
if options.fetch(:sortable, true) && !_has_sort?(attr)
590+
sort attr
591+
end
586592
end
587593

588594
def attribute_to_model_field(attribute)
@@ -669,6 +675,15 @@ def filter(attr, *args)
669675
@_allowed_filters[attr.to_sym] = args.extract_options!
670676
end
671677

678+
def sort(sorting, options = {})
679+
self._allowed_sort[sorting.to_sym] = options
680+
end
681+
682+
def sorts(*args)
683+
options = args.extract_options!
684+
_allowed_sort.merge!(args.inject({}) { |h, sorting| h[sorting.to_sym] = options.dup; h })
685+
end
686+
672687
def primary_key(key)
673688
@_primary_key = key.to_sym
674689
end
@@ -689,7 +704,7 @@ def creatable_fields(_context = nil)
689704

690705
# Override in your resource to filter the sortable keys
691706
def sortable_fields(_context = nil)
692-
_attributes.keys
707+
_allowed_sort.keys
693708
end
694709

695710
def sortable_field?(key, context = nil)
@@ -759,11 +774,7 @@ def verify_filter(filter, raw, context = nil)
759774
strategy = _allowed_filters.fetch(filter, Hash.new)[:verify]
760775

761776
if strategy
762-
if strategy.is_a?(Symbol) || strategy.is_a?(String)
763-
values = send(strategy, filter_values, context)
764-
else
765-
values = strategy.call(filter_values, context)
766-
end
777+
values = call_method_or_proc(strategy, filter_values, context)
767778
[filter, values]
768779
else
769780
if is_filter_relationship?(filter)
@@ -774,6 +785,14 @@ def verify_filter(filter, raw, context = nil)
774785
end
775786
end
776787

788+
def call_method_or_proc(strategy, *args)
789+
if strategy.is_a?(Symbol) || strategy.is_a?(String)
790+
send(strategy, *args)
791+
else
792+
strategy.call(*args)
793+
end
794+
end
795+
777796
def key_type(key_type)
778797
@_resource_key_type = key_type
779798
end
@@ -890,6 +909,10 @@ def _allowed_filters
890909
defined?(@_allowed_filters) ? @_allowed_filters : { id: {} }
891910
end
892911

912+
def _allowed_sort
913+
@_allowed_sort ||= {}
914+
end
915+
893916
def _paginator
894917
@_paginator ||= JSONAPI.configuration.default_paginator
895918
end
@@ -963,6 +986,10 @@ def _allowed_filter?(filter)
963986
!_allowed_filters[filter].nil?
964987
end
965988

989+
def _has_sort?(sorting)
990+
!_allowed_sort[sorting.to_sym].nil?
991+
end
992+
966993
def module_path
967994
if name == 'JSONAPI::Resource'
968995
''

test/controllers/controller_test.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3971,4 +3971,37 @@ def test_fetch_indicators_sort_by_widgets_name
39713971
assert_equal indicator_2.id.to_s, json_response['data'].first['id']
39723972
assert_equal 2, json_response['data'].size
39733973
end
3974+
3975+
end
3976+
3977+
class RobotsControllerTest < ActionController::TestCase
3978+
3979+
def teardown
3980+
Robot.delete_all
3981+
end
3982+
3983+
def test_fetch_robots_with_sort_by_name
3984+
Robot.create! name: 'John', version: 1
3985+
Robot.create! name: 'jane', version: 1
3986+
assert_cacheable_get :index, params: {sort: 'name'}
3987+
assert_response :success
3988+
assert_equal 'John', json_response['data'].first['attributes']['name']
3989+
end
3990+
3991+
def test_fetch_robots_with_sort_by_lower_name
3992+
Robot.create! name: 'John', version: 1
3993+
Robot.create! name: 'jane', version: 1
3994+
assert_cacheable_get :index, params: {sort: 'lower_name'}
3995+
assert_response :success
3996+
assert_equal 'jane', json_response['data'].first['attributes']['name']
3997+
end
3998+
3999+
def test_fetch_robots_with_sort_by_version
4000+
Robot.create! name: 'John', version: 1
4001+
Robot.create! name: 'jane', version: 2
4002+
assert_cacheable_get :index, params: {sort: 'version'}
4003+
assert_response 400
4004+
assert_equal 'version is not a valid sort criteria for robots', json_response['errors'].first['detail']
4005+
end
4006+
39744007
end

test/fixtures/active_record.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,12 @@
350350
t.integer :indicator_id, null: false
351351
t.timestamps null: false
352352
end
353+
354+
create_table :robots, force: true do |t|
355+
t.string :name
356+
t.integer :version
357+
t.timestamps null: false
358+
end
353359
end
354360

355361
### MODELS
@@ -725,6 +731,9 @@ class Widget < ActiveRecord::Base
725731
belongs_to :indicator
726732
end
727733

734+
class Robot < ActiveRecord::Base
735+
end
736+
728737
### CONTROLLERS
729738
class AuthorsController < JSONAPI::ResourceControllerMetal
730739
end
@@ -1021,6 +1030,9 @@ class WidgetsController < JSONAPI::ResourceController
10211030
class IndicatorsController < JSONAPI::ResourceController
10221031
end
10231032

1033+
class RobotsController < JSONAPI::ResourceController
1034+
end
1035+
10241036
### RESOURCES
10251037
class BaseResource < JSONAPI::Resource
10261038
abstract
@@ -2186,6 +2198,15 @@ class WorkerResource < JSONAPI::Resource
21862198
attribute :name
21872199
end
21882200

2201+
class RobotResource < ::JSONAPI::Resource
2202+
attribute :name
2203+
attribute :version, sortable: false
2204+
2205+
sort :lower_name, apply: ->(records, direction, _context) do
2206+
records.order("LOWER(robots.name) #{direction}")
2207+
end
2208+
end
2209+
21892210
### PORO Data - don't do this in a production app
21902211
$breed_data = BreedData.new
21912212
$breed_data.add(Breed.new(0, 'persian'))

test/test_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ class CatResource < JSONAPI::Resource
400400
jsonapi_resources :workers, only: [:show]
401401
jsonapi_resources :widgets, only: [:index]
402402
jsonapi_resources :indicators, only: [:index]
403+
jsonapi_resources :robots, only: [:index]
403404

404405
mount MyEngine::Engine => "/boomshaka", as: :my_engine
405406
mount ApiV2Engine::Engine => "/api_v2", as: :api_v2_engine

0 commit comments

Comments
 (0)