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 +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
|