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 +4 -4
- data/README.md +12 -0
- data/lib/next_page.rb +2 -0
- data/lib/next_page/exceptions.rb +11 -0
- data/lib/next_page/exceptions/invalid_nested_sort.rb +17 -0
- data/lib/next_page/exceptions/invalid_sort_parameter.rb +16 -0
- data/lib/next_page/pagination.rb +5 -4
- data/lib/next_page/paginator.rb +15 -4
- data/lib/next_page/sorter.rb +98 -0
- data/lib/next_page/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fc0be3e3856dc94bd88845bc1d794eb179bc1b848c734e69788272075ec66f39
|
4
|
+
data.tar.gz: 3069427b0ac1cb6b91ff8ebc686d739805cb6adaadb6a85badf94843d9c5d13f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/next_page.rb
CHANGED
@@ -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
|
data/lib/next_page/pagination.rb
CHANGED
@@ -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
|
-
|
56
|
-
|
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
|
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
|
68
|
+
self.class.next_page_paginator.paginate_resource(resource, params.slice(:page, :sort))
|
68
69
|
end
|
69
70
|
|
70
71
|
def render(*args) #:nodoc:
|
data/lib/next_page/paginator.rb
CHANGED
@@ -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,
|
39
|
+
def paginate_resource(data, params)
|
39
40
|
data.extend(NextPage::PaginationAttributes)
|
40
41
|
|
41
|
-
|
42
|
-
|
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
|
data/lib/next_page/version.rb
CHANGED
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.
|
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-
|
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
|