typical_sort 0.0.2.pre.rc → 0.1.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/README.md +18 -0
- data/lib/typical_sort/aggregate_sorter.rb +18 -13
- data/lib/typical_sort/params.rb +10 -14
- data/lib/typical_sort/sorter.rb +12 -3
- data/lib/typical_sort/version.rb +1 -1
- data/lib/typical_sort.rb +18 -20
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9039e103d0aab48801238a87171c7b5294780714acc3df4251f3b4e72cab03d1
|
|
4
|
+
data.tar.gz: 5717da3c8409651f5b96b2627ceeb419228cf1c643347ab7b9df0583406eb362
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a15dce023ef4db25e7b0b362681db3b7fa95c088141dd9d09c0d3d882b6d9b3f45518901cd7536a3abf3cde629815b4fc8c125b3cee1f0cb74c718f0603d8ab3
|
|
7
|
+
data.tar.gz: 1947b566ed290db9894688c3025b336b2b98138db56503f8e651c1dea90ce82694a5f9152da03a7baacb1429f038c1db1257689c11bb6ecfbbe0f600775f0d10
|
data/README.md
CHANGED
|
@@ -42,6 +42,24 @@ Supported params:
|
|
|
42
42
|
|
|
43
43
|
Only declared sorts are allowed. Unknown request sort params are ignored by default, or raised as `TypicalSort::InvalidSort` when configured with `invalid_sort = :raise`.
|
|
44
44
|
|
|
45
|
+
## Multi-sort
|
|
46
|
+
|
|
47
|
+
Pass a comma-separated list to sort by multiple fields, in priority order:
|
|
48
|
+
|
|
49
|
+
```txt
|
|
50
|
+
?sort=last_name,-created_at&sort_dir=asc
|
|
51
|
+
?sort=account.name,last_name&sort_dir=asc
|
|
52
|
+
?sort=insurance_plans.name,-created_at&sort_dir=asc
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The first example sorts by last name ascending, then creation time descending. Each field must be declared using the existing `sort` DSL; base columns, association paths, aggregates, and scope sorts can be mixed.
|
|
56
|
+
|
|
57
|
+
Direction follows the same rules as a single sort: a `-` prefix forces descending; unprefixed fields use `sort_dir`, then the controller's default direction, then the global default direction. For ascending unprefixed fields, pass `sort_dir=asc` if your default is descending.
|
|
58
|
+
|
|
59
|
+
Whitespace around entries is ignored. Repeated fields use their first occurrence. Invalid entries (including empty entries) are skipped without discarding valid sorts, or raise `TypicalSort::InvalidSort` in strict mode. The primary-key tie-breaker is appended once, after all requested sorts, unless disabled.
|
|
60
|
+
|
|
61
|
+
Scope sorts receive their field's resolved direction. Use scopes that append `order` clauses to preserve earlier sorts; a scope using `reorder` explicitly replaces preceding ordering.
|
|
62
|
+
|
|
45
63
|
## Base and association sorting
|
|
46
64
|
|
|
47
65
|
Declare base-table columns with symbols:
|
|
@@ -4,9 +4,16 @@ module TypicalSort
|
|
|
4
4
|
class AggregateSorter
|
|
5
5
|
VALID_AGGREGATES = %i[min max sum avg count directional].freeze
|
|
6
6
|
|
|
7
|
-
attr_reader :records, :relation_name, :reflection, :column_name, :direction, :definition, :configuration
|
|
7
|
+
attr_reader :records, :relation_name, :reflection, :column_name, :direction, :definition, :configuration, :table_alias
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
# Collapse collection joins once, before applying any requested ordering.
|
|
10
|
+
def self.prepare(records)
|
|
11
|
+
table = records.klass.arel_table
|
|
12
|
+
base_ids = records.except(:select, :order).select(table[records.klass.primary_key])
|
|
13
|
+
records.klass.where(records.klass.primary_key => base_ids)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(records:, relation_name:, reflection:, column_name:, direction:, definition:, configuration:, table_alias: "typical_sort_scope")
|
|
10
17
|
@records = records
|
|
11
18
|
@relation_name = relation_name.to_sym
|
|
12
19
|
@reflection = reflection
|
|
@@ -14,6 +21,7 @@ module TypicalSort
|
|
|
14
21
|
@direction = direction.to_sym
|
|
15
22
|
@definition = definition
|
|
16
23
|
@configuration = configuration
|
|
24
|
+
@table_alias = table_alias
|
|
17
25
|
end
|
|
18
26
|
|
|
19
27
|
def call
|
|
@@ -25,10 +33,6 @@ module TypicalSort
|
|
|
25
33
|
quoted_base_pk = connection.quote_column_name(base_pk)
|
|
26
34
|
sort_value_sql = value_sql(connection)
|
|
27
35
|
|
|
28
|
-
base_ids = records
|
|
29
|
-
.except(:select, :order)
|
|
30
|
-
.select("#{quoted_base_table}.#{quoted_base_pk}")
|
|
31
|
-
|
|
32
36
|
filtered_ids = records
|
|
33
37
|
.except(:select, :order, :limit, :offset, :group)
|
|
34
38
|
.select("#{quoted_base_table}.#{quoted_base_pk}")
|
|
@@ -42,13 +46,14 @@ module TypicalSort
|
|
|
42
46
|
)
|
|
43
47
|
.group("#{quoted_base_table}.#{quoted_base_pk}")
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
sort_table = Arel::Table.new(table_alias)
|
|
50
|
+
join = Arel::Nodes::OuterJoin.new(
|
|
51
|
+
sort_subquery.arel.as(table_alias),
|
|
52
|
+
Arel::Nodes::On.new(sort_table[:typical_sort_record_id].eq(records.klass.arel_table[base_pk]))
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
records.joins(join)
|
|
56
|
+
.order(Arel.sql("#{table_alias}.typical_sort_value #{sql_direction} NULLS #{nulls_position}"))
|
|
52
57
|
end
|
|
53
58
|
|
|
54
59
|
private
|
data/lib/typical_sort/params.rb
CHANGED
|
@@ -11,12 +11,20 @@ module TypicalSort
|
|
|
11
11
|
@default_direction = default_direction
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def sorts
|
|
15
|
+
attributes = raw_sort.present? ? raw_sort.to_s.split(",", -1).map(&:strip) : [default_attribute.to_s]
|
|
16
|
+
attributes.map do |attribute|
|
|
17
|
+
direction = attribute.start_with?("-") ? :desc : normalized_direction(raw_direction)
|
|
18
|
+
[attribute.delete_prefix("-"), direction || normalized_direction(default_direction) || configuration.default_direction]
|
|
19
|
+
end.uniq { |attribute, _direction| attribute }
|
|
20
|
+
end
|
|
21
|
+
|
|
14
22
|
def attribute
|
|
15
|
-
|
|
23
|
+
sorts.first&.first
|
|
16
24
|
end
|
|
17
25
|
|
|
18
26
|
def direction
|
|
19
|
-
|
|
27
|
+
sorts.first&.last
|
|
20
28
|
end
|
|
21
29
|
|
|
22
30
|
def explicit?
|
|
@@ -33,18 +41,6 @@ module TypicalSort
|
|
|
33
41
|
params[configuration.direction_param].presence
|
|
34
42
|
end
|
|
35
43
|
|
|
36
|
-
def explicit_attribute
|
|
37
|
-
return nil if raw_sort.blank?
|
|
38
|
-
|
|
39
|
-
raw_sort.to_s.delete_prefix("-")
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def explicit_direction
|
|
43
|
-
return :desc if raw_sort.to_s.start_with?("-")
|
|
44
|
-
|
|
45
|
-
normalized_direction(raw_direction)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
44
|
def normalized_direction(value)
|
|
49
45
|
return nil if value.blank?
|
|
50
46
|
|
data/lib/typical_sort/sorter.rb
CHANGED
|
@@ -5,13 +5,21 @@ require "typical_sort/path_resolver"
|
|
|
5
5
|
|
|
6
6
|
module TypicalSort
|
|
7
7
|
class Sorter
|
|
8
|
-
attr_reader :records, :definition, :direction, :configuration
|
|
8
|
+
attr_reader :records, :definition, :direction, :configuration, :sort_index
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def self.prepare(records:, definitions:)
|
|
11
|
+
aggregate = definitions.any? do |definition|
|
|
12
|
+
!definition.scope? && (definition.aggregate? || PathResolver.new(records: records, path: definition.key).collection_association?)
|
|
13
|
+
end
|
|
14
|
+
aggregate ? AggregateSorter.prepare(records) : records
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(records:, definition:, direction:, configuration:, sort_index: 0)
|
|
11
18
|
@records = records
|
|
12
19
|
@definition = definition
|
|
13
20
|
@direction = direction.to_sym
|
|
14
21
|
@configuration = configuration
|
|
22
|
+
@sort_index = sort_index
|
|
15
23
|
end
|
|
16
24
|
|
|
17
25
|
def call
|
|
@@ -28,7 +36,8 @@ module TypicalSort
|
|
|
28
36
|
column_name: resolver.column_name,
|
|
29
37
|
direction: direction,
|
|
30
38
|
definition: definition,
|
|
31
|
-
configuration: configuration
|
|
39
|
+
configuration: configuration,
|
|
40
|
+
table_alias: "typical_sort_scope_#{sort_index}"
|
|
32
41
|
).call
|
|
33
42
|
elsif resolver.relation_association?
|
|
34
43
|
sort_relation_association(resolver)
|
data/lib/typical_sort/version.rb
CHANGED
data/lib/typical_sort.rb
CHANGED
|
@@ -56,18 +56,25 @@ module TypicalSort
|
|
|
56
56
|
default_direction: default_sorting_direction
|
|
57
57
|
)
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
sorts = sort_params.sorts.filter_map do |attribute, direction|
|
|
60
|
+
definition = sort_definition_for(attribute)
|
|
61
|
+
if definition
|
|
62
|
+
[definition, direction]
|
|
63
|
+
elsif TypicalSort.configuration.invalid_sort == :raise
|
|
64
|
+
raise TypicalSort::InvalidSort, "Invalid sort: #{attribute}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
64
67
|
|
|
65
|
-
sorted = TypicalSort::Sorter.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
sorted = TypicalSort::Sorter.prepare(records: records, definitions: sorts.map(&:first))
|
|
69
|
+
sorts.each_with_index do |(definition, direction), index|
|
|
70
|
+
sorted = TypicalSort::Sorter.new(
|
|
71
|
+
records: sorted,
|
|
72
|
+
definition: definition,
|
|
73
|
+
direction: direction,
|
|
74
|
+
configuration: request_configuration,
|
|
75
|
+
sort_index: index
|
|
76
|
+
).call
|
|
77
|
+
end
|
|
71
78
|
|
|
72
79
|
append_sort_tiebreaker(sorted)
|
|
73
80
|
end
|
|
@@ -76,15 +83,6 @@ module TypicalSort
|
|
|
76
83
|
typical_sort_set.fetch(attribute)
|
|
77
84
|
end
|
|
78
85
|
|
|
79
|
-
def handle_invalid_sort(records, attribute)
|
|
80
|
-
case TypicalSort.configuration.invalid_sort
|
|
81
|
-
when :raise
|
|
82
|
-
raise TypicalSort::InvalidSort, "Invalid sort: #{attribute}"
|
|
83
|
-
else
|
|
84
|
-
append_sort_tiebreaker(records)
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
86
|
def append_sort_tiebreaker(records)
|
|
89
87
|
return records unless TypicalSort.configuration.tie_breaker == :primary_key
|
|
90
88
|
|