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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e562e9296dbb12e4cf437144bfe2b108fe07409f8b70bf844e20f69de7193908
4
- data.tar.gz: 9c3565e404b3b5bcd6eacb58ff52061b0c7f09a3a4be8350c80f48c96d3e1964
3
+ metadata.gz: 9039e103d0aab48801238a87171c7b5294780714acc3df4251f3b4e72cab03d1
4
+ data.tar.gz: 5717da3c8409651f5b96b2627ceeb419228cf1c643347ab7b9df0583406eb362
5
5
  SHA512:
6
- metadata.gz: 215dd94a7bb316c864574d106b8fae4980e8a36383408bbbdf038d6d538ac9c52e22b82d0e9f8c7d7b2e7569ea4223a250f1650ac14433aa8d8c0cbbb86b3642
7
- data.tar.gz: 4039750518702381bbe6dedffe1618dd44544307f30e46692a6204f9310099e48ef2645e858719d6beae2cf785e74474c695e8b12dcfcb88e8360b21c5906c25
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
- ## Releases
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
- ```bash
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
- ```bash
218
- bin/release pre
219
+ ```sh
220
+ bin/release {major|minor|patch|pre}
221
+ git push --follow-tags
219
222
  ```
220
223
 
221
- `pre` cycles prerelease labels in order: `alpha`, `beta`, `rc`, then the final version. For example, `1.2.0-rc` becomes `1.2.0`, tagged as `v1.2.0`.
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
- GitHub Actions publishes only when a `v*` tag is pushed. The publish workflow builds the gem and pushes it to RubyGems with `RUBYGEMS_API_KEY`.
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
- def initialize(records:, relation_name:, reflection:, column_name:, direction:, definition:, configuration:)
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
- records.klass
46
- .where(base_pk => base_ids)
47
- .joins(<<~SQL.squish)
48
- LEFT JOIN (#{sort_subquery.to_sql}) typical_sort_scope
49
- ON typical_sort_scope.typical_sort_record_id = #{quoted_base_table}.#{quoted_base_pk}
50
- SQL
51
- .order(Arel.sql("typical_sort_scope.typical_sort_value #{sql_direction} NULLS #{nulls_position}"))
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
@@ -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
- explicit_attribute || default_attribute
23
+ sorts.first&.first
16
24
  end
17
25
 
18
26
  def direction
19
- explicit_direction || normalized_direction(default_direction) || configuration.default_direction
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
 
@@ -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 initialize(records:, definition:, direction:, configuration:)
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TypicalSort
4
- VERSION = "0.0.2"
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/typical_sort.rb CHANGED
@@ -56,18 +56,25 @@ module TypicalSort
56
56
  default_direction: default_sorting_direction
57
57
  )
58
58
 
59
- attribute = sort_params.attribute
60
- return records if attribute.blank?
61
-
62
- definition = sort_definition_for(attribute)
63
- return handle_invalid_sort(records, attribute) unless definition
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.new(
66
- records: records,
67
- definition: definition,
68
- direction: sort_params.direction,
69
- configuration: request_configuration
70
- ).call
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
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typical_sort
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wyatt Kirby