next_page 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
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