jpie 2.1.5 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/json_api/controllers/base_controller.rb +5 -0
- data/lib/json_api/controllers/concerns/controller_helpers/parsing.rb +7 -9
- data/lib/json_api/controllers/concerns/relationships/sorting.rb +2 -93
- data/lib/json_api/controllers/concerns/resource_actions/field_validation.rb +0 -18
- data/lib/json_api/controllers/concerns/resource_actions.rb +0 -1
- data/lib/json_api/controllers/relationships_controller.rb +0 -2
- data/lib/json_api/resources/concerns/sortable_fields_dsl.rb +2 -9
- data/lib/json_api/support/collection_query.rb +4 -5
- data/lib/json_api/support/relationship_helpers.rb +0 -9
- data/lib/json_api/support/sort/compare.rb +33 -0
- data/lib/json_api/support/sort/field.rb +36 -0
- data/lib/json_api/support/sort.rb +90 -0
- data/lib/json_api/version.rb +1 -1
- data/lib/json_api.rb +0 -1
- metadata +4 -4
- data/lib/json_api/support/concerns/sorting.rb +0 -86
- data/lib/json_api/support/sort_parsing.rb +0 -21
- data/lib/json_api/support/sort_value_comparison.rb +0 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23bcdb19123ced67be175eeeaace99cdfed44f3ce2dcd07134ec9742cf9c1af2
|
|
4
|
+
data.tar.gz: 38d365327b7991a82512e33dac60dc2649d0bef1ac03fcd5d999519015e72dda
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: baf7cdcb438c6991aee5ca8b6181f5c4ff7ee84fc920be7777679f4ae00c22c06471860aaa40b3eac34affd82d2c509b8cd481866b37c69dbbca595155bfe2e2
|
|
7
|
+
data.tar.gz: 832ca3fb1eba8783e4bf006cb5e891650ffe711c19144dd7397aef2fd874eee7e5882e7a981d4e43e17ff666353ccf731b04ae0d1ea6945a52d2418d2d224ac9
|
data/Gemfile.lock
CHANGED
|
@@ -7,9 +7,14 @@ module JSONAPI
|
|
|
7
7
|
|
|
8
8
|
rescue_from JSONAPI::AuthorizationError, with: :render_jsonapi_authorization_error
|
|
9
9
|
rescue_from Pundit::NotAuthorizedError, with: :render_jsonapi_authorization_error if defined?(Pundit)
|
|
10
|
+
rescue_from JSONAPI::Support::Sort::InvalidFieldError, with: :render_invalid_sort_field_error
|
|
10
11
|
|
|
11
12
|
private
|
|
12
13
|
|
|
14
|
+
def render_invalid_sort_field_error(error)
|
|
15
|
+
render_sort_errors(error.invalid_fields)
|
|
16
|
+
end
|
|
17
|
+
|
|
13
18
|
def render_jsonapi_authorization_error(error)
|
|
14
19
|
detail = error&.message.presence || "You are not authorized to perform this action"
|
|
15
20
|
render json: {
|
|
@@ -110,15 +110,13 @@ module JSONAPI
|
|
|
110
110
|
params[:page].permit(:number, :size).to_h
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
-
def
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def valid_sort_fields_for_resource(resource_class)
|
|
121
|
-
resource_class.permitted_sortable_fields.map(&:to_s)
|
|
113
|
+
def render_sort_errors(invalid)
|
|
114
|
+
render_parameter_errors(
|
|
115
|
+
invalid,
|
|
116
|
+
title: "Invalid Sort Field",
|
|
117
|
+
detail_proc: ->(f) { "Invalid sort field requested: #{f}" },
|
|
118
|
+
source_proc: ->(_) { { parameter: "sort" } },
|
|
119
|
+
)
|
|
122
120
|
end
|
|
123
121
|
end
|
|
124
122
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json_api/support/
|
|
3
|
+
require "json_api/support/sort"
|
|
4
4
|
|
|
5
5
|
module JSONAPI
|
|
6
6
|
module Relationships
|
|
@@ -13,99 +13,8 @@ module JSONAPI
|
|
|
13
13
|
sorts = parse_sort_param
|
|
14
14
|
return related if sorts.empty?
|
|
15
15
|
|
|
16
|
-
related_model = association.klass
|
|
17
|
-
db_sorts, virtual_sorts = partition_sorts_by_type(sorts, related_model)
|
|
18
|
-
|
|
19
|
-
related = apply_db_sorts(related, db_sorts)
|
|
20
|
-
apply_virtual_sorts_if_needed(related, virtual_sorts, related_model)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def partition_sorts_by_type(sorts, related_model)
|
|
24
|
-
sorts.partition do |sort_field|
|
|
25
|
-
field = RelationshipHelpers.extract_sort_field_name(sort_field)
|
|
26
|
-
related_model.column_names.include?(field.to_s)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def apply_db_sorts(related, db_sorts)
|
|
31
|
-
db_sorts.each do |sort_field|
|
|
32
|
-
direction = RelationshipHelpers.extract_sort_direction(sort_field)
|
|
33
|
-
field = RelationshipHelpers.extract_sort_field_name(sort_field)
|
|
34
|
-
related = related.order(field => direction)
|
|
35
|
-
end
|
|
36
|
-
related
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def apply_virtual_sorts_if_needed(related, virtual_sorts, related_model)
|
|
40
|
-
return related unless virtual_sorts.any?
|
|
41
|
-
|
|
42
|
-
resource_class = ResourceLoader.find_for_model(related_model)
|
|
43
|
-
sort_records_by_virtual_attributes(related.to_a, virtual_sorts, resource_class)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def sort_records_by_virtual_attributes(records, virtual_sorts, resource_class)
|
|
47
|
-
records.sort do |a, b|
|
|
48
|
-
compare_records(a, b, virtual_sorts, resource_class)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def compare_records(record_a, record_b, virtual_sorts, resource_class)
|
|
53
|
-
virtual_sorts.each do |sort_field|
|
|
54
|
-
comparison = compare_by_field(record_a, record_b, sort_field, resource_class)
|
|
55
|
-
return comparison unless comparison.zero?
|
|
56
|
-
end
|
|
57
|
-
0
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def compare_by_field(record_a, record_b, sort_field, resource_class)
|
|
61
|
-
direction = RelationshipHelpers.extract_sort_direction(sort_field)
|
|
62
|
-
field = RelationshipHelpers.extract_sort_field_name(sort_field)
|
|
63
|
-
|
|
64
|
-
value_a = get_virtual_value(record_a, field, resource_class)
|
|
65
|
-
value_b = get_virtual_value(record_b, field, resource_class)
|
|
66
|
-
|
|
67
|
-
comparison = compare_values(value_a, value_b)
|
|
68
|
-
direction == :desc ? -comparison : comparison
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def get_virtual_value(record, field, resource_class)
|
|
72
|
-
resource_instance = resource_class.new(record, {})
|
|
73
|
-
field_sym = field.to_sym
|
|
74
|
-
return resource_instance.public_send(field_sym) if resource_instance.respond_to?(field_sym, false)
|
|
75
|
-
|
|
76
|
-
nil
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def compare_values(value_a, value_b)
|
|
80
|
-
JSONAPI::Support::SortValueComparison.compare(value_a, value_b)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def validate_sort_param
|
|
84
|
-
sorts = parse_sort_param
|
|
85
|
-
return if sorts.empty?
|
|
86
|
-
|
|
87
|
-
association = @resource.class.reflect_on_association(@relationship_name)
|
|
88
|
-
return unless association&.collection?
|
|
89
|
-
|
|
90
|
-
validate_sort_fields_for_association(sorts, association)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def validate_sort_fields_for_association(sorts, association)
|
|
94
16
|
resource_class = ResourceLoader.find_for_model(association.klass)
|
|
95
|
-
|
|
96
|
-
invalid_fields = invalid_sort_fields_for_columns(sorts, valid_fields)
|
|
97
|
-
return if invalid_fields.empty?
|
|
98
|
-
|
|
99
|
-
render_invalid_sort_fields(invalid_fields)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def render_invalid_sort_fields(invalid_fields)
|
|
103
|
-
render_parameter_errors(
|
|
104
|
-
invalid_fields,
|
|
105
|
-
title: "Invalid Sort Field",
|
|
106
|
-
detail_proc: ->(field) { "Invalid sort field requested: #{field}" },
|
|
107
|
-
source_proc: ->(_field) { { parameter: "sort" } },
|
|
108
|
-
)
|
|
17
|
+
Support::Sort.new(sort_params: sorts, resource_class:).apply(related)
|
|
109
18
|
end
|
|
110
19
|
end
|
|
111
20
|
end
|
|
@@ -13,15 +13,6 @@ module JSONAPI
|
|
|
13
13
|
render_field_error(error) if error
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def validate_sort_param
|
|
17
|
-
sorts = parse_sort_param
|
|
18
|
-
return if sorts.empty?
|
|
19
|
-
|
|
20
|
-
valid = valid_sort_fields_for_resource(@resource_class)
|
|
21
|
-
invalid = invalid_sort_fields_for_columns(sorts, valid)
|
|
22
|
-
render_sort_errors(invalid) if invalid.any?
|
|
23
|
-
end
|
|
24
|
-
|
|
25
16
|
private
|
|
26
17
|
|
|
27
18
|
def first_invalid_field(fields)
|
|
@@ -61,15 +52,6 @@ module JSONAPI
|
|
|
61
52
|
source_proc: ->(f) { { parameter: "fields[#{error[:type]}]=#{f}" } },
|
|
62
53
|
)
|
|
63
54
|
end
|
|
64
|
-
|
|
65
|
-
def render_sort_errors(invalid)
|
|
66
|
-
render_parameter_errors(
|
|
67
|
-
invalid,
|
|
68
|
-
title: "Invalid Sort Field",
|
|
69
|
-
detail_proc: ->(f) { "Invalid sort field requested: #{f}" },
|
|
70
|
-
source_proc: ->(_) { { parameter: "sort" } },
|
|
71
|
-
)
|
|
72
|
-
end
|
|
73
55
|
end
|
|
74
56
|
end
|
|
75
57
|
end
|
|
@@ -29,7 +29,6 @@ module JSONAPI
|
|
|
29
29
|
before_action :load_jsonapi_resource
|
|
30
30
|
before_action :validate_fields_param, only: %i[index show]
|
|
31
31
|
before_action :validate_filter_param, only: [:index]
|
|
32
|
-
before_action :validate_sort_param, only: [:index]
|
|
33
32
|
before_action :validate_include_param, only: %i[index show]
|
|
34
33
|
before_action :validate_resource_type!, only: %i[create update]
|
|
35
34
|
before_action :validate_resource_id!, only: [:update]
|
|
@@ -25,8 +25,6 @@ module JSONAPI
|
|
|
25
25
|
before_action :set_resource
|
|
26
26
|
before_action :set_relationship_name
|
|
27
27
|
before_action :validate_relationship_exists
|
|
28
|
-
before_action :validate_sort_param, only: [:show]
|
|
29
|
-
|
|
30
28
|
def show
|
|
31
29
|
authorize_resource_action!(@resource, action: :show, context: { relationship: @relationship_name })
|
|
32
30
|
render json: build_show_response, status: :ok
|
|
@@ -14,22 +14,15 @@ module JSONAPI
|
|
|
14
14
|
|
|
15
15
|
def permitted_sortable_fields
|
|
16
16
|
sort_fields = @sortable_fields || []
|
|
17
|
-
sort_fields =
|
|
18
|
-
|
|
17
|
+
sort_fields = superclass.permitted_sortable_fields + sort_fields if should_inherit_sortable_fields?
|
|
18
|
+
sort_fields.uniq
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def should_inherit_sortable_fields?
|
|
22
22
|
!instance_variable_defined?(:@sortable_fields) &&
|
|
23
|
-
!instance_variable_defined?(:@attributes) &&
|
|
24
23
|
superclass != JSONAPI::Resource &&
|
|
25
24
|
superclass.respond_to?(:permitted_sortable_fields)
|
|
26
25
|
end
|
|
27
|
-
|
|
28
|
-
def inherited_sort_only_fields
|
|
29
|
-
parent_sort_fields = superclass.permitted_sortable_fields
|
|
30
|
-
parent_attributes = superclass.permitted_attributes
|
|
31
|
-
parent_sort_fields - parent_attributes
|
|
32
|
-
end
|
|
33
26
|
end
|
|
34
27
|
end
|
|
35
28
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "sort"
|
|
4
4
|
require_relative "concerns/regular_filters"
|
|
5
5
|
require_relative "concerns/nested_filters"
|
|
6
6
|
require_relative "concerns/polymorphic_filters"
|
|
@@ -9,7 +9,6 @@ require_relative "concerns/pagination"
|
|
|
9
9
|
|
|
10
10
|
module JSONAPI
|
|
11
11
|
class CollectionQuery
|
|
12
|
-
include Support::Sorting
|
|
13
12
|
include Support::RegularFilters
|
|
14
13
|
include Support::NestedFilters
|
|
15
14
|
include Support::PolymorphicFilters
|
|
@@ -30,8 +29,9 @@ module JSONAPI
|
|
|
30
29
|
|
|
31
30
|
def execute
|
|
32
31
|
@scope = apply_filtering
|
|
32
|
+
@sort = Support::Sort.new(sort_params:, resource_class: definition)
|
|
33
33
|
compute_total_count_if_needed
|
|
34
|
-
@scope =
|
|
34
|
+
@scope = @sort.apply(@scope)
|
|
35
35
|
@scope = apply_pagination
|
|
36
36
|
self
|
|
37
37
|
end
|
|
@@ -43,8 +43,7 @@ module JSONAPI
|
|
|
43
43
|
def compute_total_count_if_needed
|
|
44
44
|
return unless pagination_applied
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
@total_count = @scope.count if !has_virtual_sort || @scope.is_a?(Array)
|
|
46
|
+
@total_count = @scope.count if !@sort.virtual_fields? || @scope.is_a?(Array)
|
|
48
47
|
end
|
|
49
48
|
|
|
50
49
|
def apply_filtering
|
|
@@ -17,15 +17,6 @@ module JSONAPI
|
|
|
17
17
|
TypeConversion.resource_type_name(definition_class)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
# Delegate to SortParsing
|
|
21
|
-
def extract_sort_field_name(sort_field)
|
|
22
|
-
SortParsing.extract_sort_field_name(sort_field)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def extract_sort_direction(sort_field)
|
|
26
|
-
SortParsing.extract_sort_direction(sort_field)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
20
|
# Delegate to ResourceIdentifier
|
|
30
21
|
# rubocop:disable Lint/UnusedMethodArgument
|
|
31
22
|
def serialize_resource_identifier(
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Support
|
|
5
|
+
class Sort
|
|
6
|
+
# Compares two values for stable sort ordering. Ruby 3.4+ TrueClass#<=> returns nil
|
|
7
|
+
# when operands differ (true <=> false), so booleans are normalized before <=>.
|
|
8
|
+
module Compare
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def compare(value_a, value_b)
|
|
12
|
+
return 0 if value_a.nil? && value_b.nil?
|
|
13
|
+
return -1 if value_a.nil?
|
|
14
|
+
return 1 if value_b.nil?
|
|
15
|
+
|
|
16
|
+
comparable(value_a) <=> comparable(value_b) ||
|
|
17
|
+
raise_incomparable(value_a, value_b)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def comparable(value)
|
|
21
|
+
return value ? 1 : 0 if value in TrueClass | FalseClass
|
|
22
|
+
|
|
23
|
+
value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def raise_incomparable(value_a, value_b)
|
|
27
|
+
detail = "#{value_a.class.name} (#{value_a.inspect}), #{value_b.class.name} (#{value_b.inspect})"
|
|
28
|
+
raise ArgumentError, "incomparable sort values: #{detail}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Support
|
|
5
|
+
class Sort
|
|
6
|
+
class Field
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
attr_reader :name, :direction
|
|
10
|
+
|
|
11
|
+
def initialize(model_class, sort_field)
|
|
12
|
+
@model_class = model_class
|
|
13
|
+
@name = sort_field.delete_prefix("-")
|
|
14
|
+
@direction = sort_field.start_with?("-") ? :desc : :asc
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def virtual? = !(column? || scope?)
|
|
18
|
+
|
|
19
|
+
def apply(scope)
|
|
20
|
+
raise Error, "cannot chain a virtual sort" if virtual?
|
|
21
|
+
|
|
22
|
+
if column?
|
|
23
|
+
scope.order(name => direction)
|
|
24
|
+
else
|
|
25
|
+
scope.public_send(name.to_sym, direction)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def column? = @model_class.column_names.include?(name.to_s)
|
|
32
|
+
def scope? = @model_class.respond_to?(name.to_sym)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "sort/field"
|
|
4
|
+
require_relative "sort/compare"
|
|
5
|
+
|
|
6
|
+
module JSONAPI
|
|
7
|
+
module Support
|
|
8
|
+
# Applies sort parameters to a scope, handling database column sorts,
|
|
9
|
+
# scope-based sorts, and virtual attribute sorts (in-memory).
|
|
10
|
+
#
|
|
11
|
+
# Validates sort_params against resource_class.permitted_sortable_fields
|
|
12
|
+
# eagerly in the constructor. Raises InvalidFieldError if any fields are
|
|
13
|
+
# not permitted — callers never need to validate separately.
|
|
14
|
+
#
|
|
15
|
+
# Virtual sorts (resource instance methods) cannot be applied at the
|
|
16
|
+
# database level, so they are collected and applied last as an in-memory
|
|
17
|
+
# sort over the materialised result set. This means virtual sorts always
|
|
18
|
+
# take precedence over column/scope sorts regardless of the order the
|
|
19
|
+
# user specified them in.
|
|
20
|
+
class Sort
|
|
21
|
+
class InvalidFieldError < JSONAPI::Error
|
|
22
|
+
attr_reader :invalid_fields
|
|
23
|
+
|
|
24
|
+
def initialize(invalid_fields)
|
|
25
|
+
@invalid_fields = invalid_fields
|
|
26
|
+
super("Invalid sort fields: #{invalid_fields.join(", ")}")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(sort_params:, resource_class:)
|
|
31
|
+
@fields = build_fields(sort_params, resource_class)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def apply(scope)
|
|
35
|
+
return scope if @fields.empty?
|
|
36
|
+
|
|
37
|
+
virtual, db = @fields.partition(&:virtual?)
|
|
38
|
+
scope = db.reduce(scope) { |acc, field| field.apply(acc) }
|
|
39
|
+
virtual.any? ? apply_virtual_sorting(scope, virtual) : scope
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def virtual_fields?
|
|
43
|
+
@fields.any?(&:virtual?)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
attr_reader :resource_class
|
|
49
|
+
|
|
50
|
+
def build_fields(sort_params, resource_class)
|
|
51
|
+
return [] if sort_params.empty?
|
|
52
|
+
|
|
53
|
+
@resource_class = resource_class
|
|
54
|
+
validate!(sort_params, resource_class)
|
|
55
|
+
sort_params.map { Field.new(resource_class.model_class, it) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def validate!(sort_params, resource_class)
|
|
59
|
+
permitted = resource_class.permitted_sortable_fields.map(&:to_s)
|
|
60
|
+
invalid = sort_params.filter_map do |sort_field|
|
|
61
|
+
name = sort_field.delete_prefix("-")
|
|
62
|
+
name unless permitted.include?(name.to_s)
|
|
63
|
+
end
|
|
64
|
+
raise InvalidFieldError, invalid if invalid.any?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def apply_virtual_sorting(scope, virtual_fields)
|
|
68
|
+
scope.to_a.sort { |a, b| compare_by_fields(a, b, virtual_fields) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def compare_by_fields(record_a, record_b, fields)
|
|
72
|
+
fields.each do |field|
|
|
73
|
+
value_a = virtual_value(record_a, field.name)
|
|
74
|
+
value_b = virtual_value(record_b, field.name)
|
|
75
|
+
comparison = Compare.compare(value_a, value_b)
|
|
76
|
+
next if comparison.zero?
|
|
77
|
+
|
|
78
|
+
return field.direction == :desc ? -comparison : comparison
|
|
79
|
+
end
|
|
80
|
+
0
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def virtual_value(record, field_name)
|
|
84
|
+
instance = resource_class.new(record, {})
|
|
85
|
+
field_sym = field_name.to_sym
|
|
86
|
+
instance.respond_to?(field_sym, false) ? instance.public_send(field_sym) : nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/json_api/version.rb
CHANGED
data/lib/json_api.rb
CHANGED
|
@@ -28,7 +28,6 @@ end
|
|
|
28
28
|
require "json_api/resources/resource"
|
|
29
29
|
require "json_api/resources/resource_loader"
|
|
30
30
|
require "json_api/support/type_conversion"
|
|
31
|
-
require "json_api/support/sort_parsing"
|
|
32
31
|
require "json_api/support/resource_identifier"
|
|
33
32
|
require "json_api/support/relationship_helpers"
|
|
34
33
|
require "json_api/support/param_helpers"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jpie
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Emil Kampp
|
|
@@ -178,7 +178,6 @@ files:
|
|
|
178
178
|
- lib/json_api/support/concerns/pagination.rb
|
|
179
179
|
- lib/json_api/support/concerns/polymorphic_filters.rb
|
|
180
180
|
- lib/json_api/support/concerns/regular_filters.rb
|
|
181
|
-
- lib/json_api/support/concerns/sorting.rb
|
|
182
181
|
- lib/json_api/support/correlation_id.rb
|
|
183
182
|
- lib/json_api/support/filter_parsing.rb
|
|
184
183
|
- lib/json_api/support/header_warning_subscriber.rb
|
|
@@ -192,8 +191,9 @@ files:
|
|
|
192
191
|
- lib/json_api/support/relationship_helpers.rb
|
|
193
192
|
- lib/json_api/support/resource_identifier.rb
|
|
194
193
|
- lib/json_api/support/responders.rb
|
|
195
|
-
- lib/json_api/support/
|
|
196
|
-
- lib/json_api/support/
|
|
194
|
+
- lib/json_api/support/sort.rb
|
|
195
|
+
- lib/json_api/support/sort/compare.rb
|
|
196
|
+
- lib/json_api/support/sort/field.rb
|
|
197
197
|
- lib/json_api/support/type_conversion.rb
|
|
198
198
|
- lib/json_api/testing.rb
|
|
199
199
|
- lib/json_api/testing/rspec.rb
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json_api/support/sort_value_comparison"
|
|
4
|
-
|
|
5
|
-
module JSONAPI
|
|
6
|
-
module Support
|
|
7
|
-
module Sorting
|
|
8
|
-
def apply_sorting(scope)
|
|
9
|
-
return scope if sort_params.empty?
|
|
10
|
-
|
|
11
|
-
has_virtual_sorts = sort_params.any? { |sort_field| virtual_attribute_sort?(sort_field) }
|
|
12
|
-
has_virtual_sorts ? apply_virtual_sorting(scope) : apply_db_sorting(scope)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
def apply_virtual_sorting(scope)
|
|
18
|
-
records = scope.to_a
|
|
19
|
-
apply_mixed_sorting(records, sort_params)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def apply_db_sorting(scope)
|
|
23
|
-
sort_params.each do |sort_field|
|
|
24
|
-
direction = RelationshipHelpers.extract_sort_direction(sort_field)
|
|
25
|
-
field = RelationshipHelpers.extract_sort_field_name(sort_field)
|
|
26
|
-
scope = scope.order(field => direction)
|
|
27
|
-
end
|
|
28
|
-
scope
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def virtual_attribute_sort?(sort_field)
|
|
32
|
-
field = RelationshipHelpers.extract_sort_field_name(sort_field)
|
|
33
|
-
!model_class.column_names.include?(field.to_s)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def apply_mixed_sorting(records, all_sorts)
|
|
37
|
-
records.sort do |a, b|
|
|
38
|
-
compare_by_sort_criteria(a, b, all_sorts)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def compare_by_sort_criteria(record_a, record_b, all_sorts)
|
|
43
|
-
all_sorts.each do |sort_field|
|
|
44
|
-
direction = RelationshipHelpers.extract_sort_direction(sort_field)
|
|
45
|
-
field = RelationshipHelpers.extract_sort_field_name(sort_field)
|
|
46
|
-
value_a = fetch_sort_value(record_a, field)
|
|
47
|
-
value_b = fetch_sort_value(record_b, field)
|
|
48
|
-
comparison = compare_values(value_a, value_b)
|
|
49
|
-
next if comparison.zero?
|
|
50
|
-
|
|
51
|
-
return direction == :desc ? -comparison : comparison
|
|
52
|
-
end
|
|
53
|
-
0
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def fetch_sort_value(record, field)
|
|
57
|
-
return fetch_column_value(record, field) if model_class.column_names.include?(field.to_s)
|
|
58
|
-
|
|
59
|
-
fetch_virtual_attribute_value(record, field)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def fetch_column_value(record, field)
|
|
63
|
-
return record.public_send(field.to_sym) if record.respond_to?(field.to_sym)
|
|
64
|
-
return nil unless record.respond_to?(:attributes) && record.attributes.is_a?(Hash)
|
|
65
|
-
|
|
66
|
-
record.attributes[field.to_s] || record.attributes[field.to_sym]
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def fetch_virtual_attribute_value(record, field)
|
|
70
|
-
definition_instance = definition.new(record, {})
|
|
71
|
-
fetch_virtual_value(definition_instance, field)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def fetch_virtual_value(definition_instance, field)
|
|
75
|
-
field_sym = field.to_sym
|
|
76
|
-
return definition_instance.public_send(field_sym) if definition_instance.respond_to?(field_sym, false)
|
|
77
|
-
|
|
78
|
-
nil
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def compare_values(value_a, value_b)
|
|
82
|
-
SortValueComparison.compare(value_a, value_b)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module JSONAPI
|
|
4
|
-
module SortParsing
|
|
5
|
-
module_function
|
|
6
|
-
|
|
7
|
-
def parse_sort_field(sort_field)
|
|
8
|
-
descending = sort_field.start_with?("-")
|
|
9
|
-
field = descending ? sort_field[1..] : sort_field
|
|
10
|
-
{ field:, direction: descending ? :desc : :asc }
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def extract_sort_field_name(sort_field)
|
|
14
|
-
parse_sort_field(sort_field)[:field]
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def extract_sort_direction(sort_field)
|
|
18
|
-
parse_sort_field(sort_field)[:direction]
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module JSONAPI
|
|
4
|
-
module Support
|
|
5
|
-
# Compares two values for stable sort ordering. Ruby 3.4+ TrueClass#<=> returns nil
|
|
6
|
-
# when operands differ (true <=> false), so booleans are normalized before <=>.
|
|
7
|
-
module SortValueComparison
|
|
8
|
-
module_function
|
|
9
|
-
|
|
10
|
-
def compare(value_a, value_b)
|
|
11
|
-
return 0 if value_a.nil? && value_b.nil?
|
|
12
|
-
return -1 if value_a.nil?
|
|
13
|
-
return 1 if value_b.nil?
|
|
14
|
-
|
|
15
|
-
value_a = value_a ? 1 : 0 if value_a in TrueClass | FalseClass
|
|
16
|
-
value_b = value_b ? 1 : 0 if value_b in TrueClass | FalseClass
|
|
17
|
-
|
|
18
|
-
result = value_a <=> value_b
|
|
19
|
-
return result unless result.nil?
|
|
20
|
-
|
|
21
|
-
detail = "#{value_a.class.name} (#{value_a.inspect}), #{value_b.class.name} (#{value_b.inspect})"
|
|
22
|
-
raise ArgumentError, "incomparable sort values: #{detail}"
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|