typical_sort 0.0.2 → 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 +25 -24
- 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:
|
|
@@ -194,35 +212,18 @@ bundle exec appraisal rspec
|
|
|
194
212
|
bundle exec standardrb
|
|
195
213
|
```
|
|
196
214
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
Releases are driven by git tags. The version lives in `lib/typical_sort/version.rb`, and the gemspec reads `TypicalSort::VERSION`.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
```bash
|
|
203
|
-
bundle install
|
|
204
|
-
bin/release patch # or: minor, major
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
To publish an RC for a specific version:
|
|
215
|
+
### Releasing
|
|
208
216
|
|
|
209
|
-
|
|
210
|
-
bin/release rc 1.2.0
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
This sets the version to `1.2.0-rc` and tags it as `v1.2.0-rc`.
|
|
214
|
-
|
|
215
|
-
Prereleases can also use `bump`'s native `pre` increment:
|
|
217
|
+
Create a release from `main`:
|
|
216
218
|
|
|
217
|
-
```
|
|
218
|
-
bin/release pre
|
|
219
|
+
```sh
|
|
220
|
+
bin/release {major|minor|patch|pre}
|
|
221
|
+
git push --follow-tags
|
|
219
222
|
```
|
|
220
223
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
`bin/release` uses `bump`, commits the version file, creates a `v<version>` tag, pushes the branch, and pushes the tag.
|
|
224
|
+
The release script validates the repository, bumps the version, creates a git tag.
|
|
224
225
|
|
|
225
|
-
|
|
226
|
+
Publishing to RubyGems and creating a GitHub Release are handled automatically by GitHub Actions.
|
|
226
227
|
|
|
227
228
|
## Contributing
|
|
228
229
|
|
|
@@ -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
|
|