next_page 0.1.3 → 0.1.4

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: 9789ab6b725aa655241fcfa9b43e66152b516a58b741088fd7797fe571a23915
4
- data.tar.gz: da2e4c3608a52b1286ee33aef1005a5f4ab229fc579cd99b7c86c3177409423d
3
+ metadata.gz: fc0be3e3856dc94bd88845bc1d794eb179bc1b848c734e69788272075ec66f39
4
+ data.tar.gz: 3069427b0ac1cb6b91ff8ebc686d739805cb6adaadb6a85badf94843d9c5d13f
5
5
  SHA512:
6
- metadata.gz: c7c72b0294f2f4f4f62208fa3fc9671fd67e342c09e37338ff1df6c9596ef09a935e4251a5d0ec9870aada9d451eb724efe2a213f6406d39e75baab1327b99fb
7
- data.tar.gz: 9c8362bfcc1fa2694835fe30aa78afa43f957d7269bb0e2cad787cb452bb55582d36a064031061061640a55ff6553bd6f65c427cbe67c709b6be75eaf43997d7
6
+ metadata.gz: 74f6194348047238078e0b0fe3f5dd3a508b4a2d2a8e260511b19da571b40b187ea8fe79a363becf95f115945c1bedc4e79016bbf43d007ced9788e14924b1b8
7
+ data.tar.gz: 07db432a16ad366222ce3da4531203535e918be0562ce63c9a6d2fe852f1ad3f4dd90af8e53f2941428374abdfe2fa20397a11bfecc1f1d8633ab4a394fe83c1
data/README.md CHANGED
@@ -43,6 +43,18 @@ resource:
43
43
  @photos = paginate_resource(@photos)
44
44
  ```
45
45
 
46
+ ### Sorting
47
+ Requests can specify sort order using the parameter `sort` with an attribute name, scope, or nested attribute. Attributes and nested attributes can be prefixed with `+` or `-` to indicate ascending or descending. Multiple sorts can be specified either as a comma separated list or via bracket notation.
48
+
49
+ /photos?sort=-created_at
50
+ /photos?sort=location,-created_by
51
+ /photos?sort[]=location&photos[]=-created_by
52
+
53
+ The default sort order is primary key descending. It can be overridden by using the `default_sort` option of `paginate_with`. Use a string formatted just as url parameter would be formatted.
54
+
55
+ ```ruby
56
+ paginate_with default_sort: '-created_at'
57
+ ```
46
58
 
47
59
  ### Default Limit
48
60
  The default size limit can be overridden with the `paginate_with` method for either type of paginagion. Pass option
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'next_page/exceptions'
3
4
  require 'next_page/pagination'
4
5
  require 'next_page/pagination_attributes'
6
+ require 'next_page/sorter'
5
7
  require 'next_page/paginator'
6
8
 
7
9
  # = Next Page
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NextPage
4
+ module Exceptions
5
+ class NextPageError < StandardError
6
+ end
7
+ end
8
+ end
9
+
10
+ require 'next_page/exceptions/invalid_nested_sort'
11
+ require 'next_page/exceptions/invalid_sort_parameter'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NextPage
4
+ module Exceptions
5
+ # = Invalid Nested Sort
6
+ class InvalidNestedSort < NextPage::Exceptions::NextPageError
7
+ def initialize(model, association)
8
+ @model = model
9
+ @association = association
10
+ end
11
+
12
+ def message
13
+ "Invalid nested sort: Unable to find association #{@association} on model #{@model}"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NextPage
4
+ module Exceptions
5
+ # = Invalid Sort Parameter
6
+ class InvalidSortParameter < NextPage::Exceptions::NextPageError
7
+ def initialize(segment)
8
+ @segment = segment
9
+ end
10
+
11
+ def message
12
+ "Invalid sort parameter (#{@segment}). Must be an attribute or scope."
13
+ end
14
+ end
15
+ end
16
+ end
@@ -52,19 +52,20 @@ module NextPage
52
52
  # - instance_variable_name: explicitly name the variable if it does not follow the convention
53
53
  # - model_class: explicitly specify the model name if it does not follow the convention
54
54
  # - default_limit: specify an alternate default
55
- def paginate_with(instance_variable_name: nil, model_class: nil, default_limit: nil)
56
- next_page_paginator.paginate_with(instance_variable_name, model_class, default_limit)
55
+ # - default_sort: sort parameter if none provided, use same format as url: created_at OR -updated_at
56
+ def paginate_with(instance_variable_name: nil, model_class: nil, default_limit: nil, default_sort: nil)
57
+ next_page_paginator.paginate_with(instance_variable_name, model_class, default_limit, default_sort)
57
58
  end
58
59
  end
59
60
 
60
61
  # Called with before_action in order to automatically paginate the resource.
61
62
  def apply_next_page_pagination
62
- self.class.next_page_paginator.paginate(self, params[:page])
63
+ self.class.next_page_paginator.paginate(self, params.slice(:page, :sort))
63
64
  end
64
65
 
65
66
  # Invokes pagination directly, the result must be stored as the resource itself is not modified.
66
67
  def paginate_resource(resource)
67
- self.class.next_page_paginator.paginate_resource(resource, params[:page])
68
+ self.class.next_page_paginator.paginate_resource(resource, params.slice(:page, :sort))
68
69
  end
69
70
 
70
71
  def render(*args) #:nodoc:
@@ -22,10 +22,11 @@ module NextPage
22
22
  @default_limit = DEFAULT_LIMIT
23
23
  end
24
24
 
25
- def paginate_with(instance_variable_name, model_class, default_limit)
25
+ def paginate_with(instance_variable_name, model_class, default_limit, default_sort)
26
26
  @default_limit = default_limit if default_limit.present?
27
27
  @instance_variable_name = instance_variable_name
28
28
  @model_class = model_class.is_a?(String) ? model_class.constantize : model_class
29
+ @default_sort = default_sort
29
30
  end
30
31
 
31
32
  def paginate(controller, page_params)
@@ -35,11 +36,13 @@ module NextPage
35
36
  controller.instance_variable_set(name, paginate_resource(data, page_params))
36
37
  end
37
38
 
38
- def paginate_resource(data, page_params)
39
+ def paginate_resource(data, params)
39
40
  data.extend(NextPage::PaginationAttributes)
40
41
 
41
- limit = page_size(page_params)
42
- offset = page_number(page_params) - 1
42
+ data = sorter.sort(data, params.fetch(:sort, default_sort))
43
+
44
+ limit = page_size(params[:page])
45
+ offset = page_number(params[:page]) - 1
43
46
  data.limit(limit).offset(offset * limit)
44
47
  end
45
48
 
@@ -55,6 +58,10 @@ module NextPage
55
58
  @instance_variable_name ||= @controller_name
56
59
  end
57
60
 
61
+ def default_sort
62
+ @default_sort ||= "-#{@model_class.primary_key}"
63
+ end
64
+
58
65
  def page_size(page)
59
66
  if page.present? && page[:size].present?
60
67
  page[:size]&.to_i
@@ -70,5 +77,9 @@ module NextPage
70
77
  1
71
78
  end
72
79
  end
80
+
81
+ def sorter
82
+ @sorter ||= NextPage::Sorter.new(model_class)
83
+ end
73
84
  end
74
85
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NextPage
4
+ # = Sorter
5
+ #
6
+ # Class Sorter reads the sort parameter and applies the related ordering. Results for each parameter string are
7
+ # cached so evaluation only occurs once.
8
+ class Sorter
9
+ SEGMENT_REGEX = /(?<sign>[+|-]?)(?<attribute>\w+)/.freeze
10
+
11
+ # Initializes a new sorter. The given model is used to validate sort attributes as well as build nested sorts.
12
+ def initialize(model)
13
+ @model = model
14
+ @cache = Hash.new { |hash, key| hash[key] = build_sort(key) }
15
+ end
16
+
17
+ # Adds sorting to given query based upon the param. Returns a new query; the existing query is NOT modified.
18
+ #
19
+ # The +query+ parameter is an ActiveRecord arel or model.
20
+ #
21
+ # The +sort_fields+ parameter is a string that conforms to the JSON-API specification for sorting fields:
22
+ # https://jsonapi.org/format/#fetching-sorting
23
+ def sort(query, sort_fields)
24
+ return from_array(query, sort_fields.split(',')) if sort_fields.include?(',')
25
+ return from_array(query, sort_fields) if sort_fields.is_a? Array
26
+
27
+ apply_sort(query, sort_fields)
28
+ end
29
+
30
+ private
31
+
32
+ def apply_sort(query, key)
33
+ @cache[key].call(query)
34
+ end
35
+
36
+ def from_array(query, param)
37
+ param.reduce(query) { |memo, key| apply_sort(memo, key) }
38
+ end
39
+
40
+ # returns a lambda that applies the appropriate sort, either from a scope, nested attribute, or attribute
41
+ def build_sort(key)
42
+ ActiveSupport::Notifications.instrument('build_sort.next_page', { key: key }) do
43
+ if @model.respond_to?(key)
44
+ ->(query) { query.send(key) }
45
+ elsif key.include?('.')
46
+ build_nested_sort(key)
47
+ else
48
+ order_params = directional_attribute(@model, key)
49
+ ->(query) { query.order(order_params) }
50
+ end
51
+ end
52
+ end
53
+
54
+ def build_nested_sort(nested_key)
55
+ # remove and capture sign if present
56
+ sign = nil
57
+ if nested_key.start_with?('+', '-')
58
+ sign = nested_key[0]
59
+ nested_key = nested_key[1..-1]
60
+ end
61
+
62
+ *associations, key = *nested_key.split('.')
63
+ sort_model = dig_association_model(associations)
64
+ joins = build_joins(associations)
65
+ order_params = directional_attribute(sort_model, "#{sign}#{key}")
66
+
67
+ ->(query) { query.joins(joins).merge(sort_model.order(order_params)) }
68
+ end
69
+
70
+ # traverse nested associations to find last association's model
71
+ def dig_association_model(associations)
72
+ associations.reduce(@model) do |model, association_name|
73
+ association = model.reflect_on_association(association_name)
74
+ raise NextPage::Exceptions::InvalidNestedSort.new(model, association_name) if association.nil?
75
+
76
+ association.klass
77
+ end
78
+ end
79
+
80
+ # transform associations array to nested hash
81
+ # ['team'] => [:team]
82
+ # ['team', 'coach'] => { team: :coach }
83
+ def build_joins(associations)
84
+ associations.map(&:to_sym)
85
+ .reverse
86
+ .reduce { |memo, association| memo.nil? ? association.to_sym : { association => memo } }
87
+ end
88
+
89
+ def directional_attribute(model, segment)
90
+ parsed = segment.match SEGMENT_REGEX
91
+ attribute = parsed['attribute']
92
+ direction = parsed['sign'] == '-' ? 'desc' : 'asc'
93
+ return { attribute => direction } if model.attribute_names.include?(attribute)
94
+
95
+ raise NextPage::Exceptions::InvalidSortParameter, segment
96
+ end
97
+ end
98
+ end
@@ -1,3 +1,3 @@
1
1
  module NextPage
2
- VERSION = '0.1.3'
2
+ VERSION = '0.1.4'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: next_page
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Todd Kummer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-05 00:00:00.000000000 Z
11
+ date: 2020-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -140,9 +140,13 @@ files:
140
140
  - README.md
141
141
  - Rakefile
142
142
  - lib/next_page.rb
143
+ - lib/next_page/exceptions.rb
144
+ - lib/next_page/exceptions/invalid_nested_sort.rb
145
+ - lib/next_page/exceptions/invalid_sort_parameter.rb
143
146
  - lib/next_page/pagination.rb
144
147
  - lib/next_page/pagination_attributes.rb
145
148
  - lib/next_page/paginator.rb
149
+ - lib/next_page/sorter.rb
146
150
  - lib/next_page/version.rb
147
151
  - lib/tasks/next_page_tasks.rake
148
152
  homepage: https://github.com/RockSolt/next_page