pagy 8.6.0 → 9.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/apps/calendar.ru +5 -5
- data/apps/demo.ru +8 -9
- data/apps/keyset_ar.ru +236 -0
- data/apps/keyset_s.ru +238 -0
- data/apps/rails.ru +7 -9
- data/apps/repro.ru +5 -6
- data/apps/tmp/calendar.sqlite3 +0 -0
- data/apps/tmp/calendar.sqlite3-shm +0 -0
- data/apps/tmp/calendar.sqlite3-wal +0 -0
- data/apps/tmp/local_secret.txt +1 -1
- data/apps/tmp/pagy-keyset-ar.sqlite3 +0 -0
- data/apps/tmp/{pagy-rails.sqlite3 → pagy-keyset-ar.sqlite3-shm} +0 -0
- data/apps/tmp/pagy-keyset-ar.sqlite3-wal +0 -0
- data/apps/tmp/pagy-keyset-s.sqlite3 +0 -0
- data/bin/pagy +4 -2
- data/config/pagy.rb +34 -31
- data/javascripts/pagy-module.js +1 -1
- data/javascripts/pagy.js +2 -2
- data/javascripts/pagy.min.js +2 -2
- data/javascripts/pagy.min.js.map +3 -3
- data/javascripts/pagy.mjs +2 -2
- data/lib/pagy/b64.rb +33 -0
- data/lib/pagy/backend.rb +21 -17
- data/lib/pagy/calendar/day.rb +1 -1
- data/lib/pagy/calendar/month.rb +1 -1
- data/lib/pagy/calendar/quarter.rb +1 -1
- data/lib/pagy/calendar/unit.rb +7 -10
- data/lib/pagy/calendar/week.rb +1 -1
- data/lib/pagy/calendar/year.rb +1 -1
- data/lib/pagy/calendar.rb +5 -5
- data/lib/pagy/countless.rb +11 -15
- data/lib/pagy/extras/arel.rb +8 -10
- data/lib/pagy/extras/array.rb +4 -6
- data/lib/pagy/extras/bootstrap.rb +5 -5
- data/lib/pagy/extras/bulma.rb +10 -7
- data/lib/pagy/extras/calendar.rb +4 -5
- data/lib/pagy/extras/countless.rb +15 -13
- data/lib/pagy/extras/elasticsearch_rails.rb +15 -15
- data/lib/pagy/extras/gearbox.rb +26 -26
- data/lib/pagy/extras/headers.rb +25 -24
- data/lib/pagy/extras/js_tools.rb +8 -8
- data/lib/pagy/extras/jsonapi.rb +26 -16
- data/lib/pagy/extras/keyset.rb +36 -0
- data/lib/pagy/extras/limit.rb +63 -0
- data/lib/pagy/extras/meilisearch.rb +11 -11
- data/lib/pagy/extras/metadata.rb +2 -2
- data/lib/pagy/extras/overflow.rb +6 -6
- data/lib/pagy/extras/pagy.rb +16 -16
- data/lib/pagy/extras/searchkick.rb +11 -11
- data/lib/pagy/extras/size.rb +2 -2
- data/lib/pagy/extras/standalone.rb +6 -6
- data/lib/pagy/extras/trim.rb +2 -2
- data/lib/pagy/frontend.rb +32 -33
- data/lib/pagy/i18n.rb +1 -1
- data/lib/pagy/keyset/active_record.rb +38 -0
- data/lib/pagy/keyset/sequel.rb +51 -0
- data/lib/pagy/keyset.rb +98 -0
- data/lib/pagy/shared_methods.rb +27 -0
- data/lib/pagy/url_helpers.rb +4 -4
- data/lib/pagy.rb +54 -68
- data/locales/ar.yml +2 -1
- data/locales/be.yml +1 -1
- data/locales/bg.yml +1 -1
- data/locales/bs.yml +1 -1
- data/locales/ca.yml +1 -1
- data/locales/ckb.yml +1 -1
- data/locales/cs.yml +1 -1
- data/locales/da.yml +1 -1
- data/locales/de.yml +1 -1
- data/locales/en.yml +1 -1
- data/locales/es.yml +1 -1
- data/locales/fr.yml +1 -1
- data/locales/hr.yml +1 -1
- data/locales/id.yml +1 -1
- data/locales/it.yml +1 -1
- data/locales/ja.yml +1 -1
- data/locales/km.yml +1 -1
- data/locales/ko.yml +1 -1
- data/locales/nb.yml +1 -1
- data/locales/nl.yml +1 -1
- data/locales/nn.yml +1 -1
- data/locales/pl.yml +1 -1
- data/locales/pt-BR.yml +1 -1
- data/locales/pt.yml +1 -1
- data/locales/ru.yml +1 -1
- data/locales/sr.yml +1 -1
- data/locales/sv-SE.yml +1 -1
- data/locales/sv.yml +1 -1
- data/locales/sw.yml +1 -1
- data/locales/ta.yml +1 -1
- data/locales/tr.yml +1 -1
- data/locales/uk.yml +1 -1
- data/locales/vi.yml +1 -1
- data/locales/zh-CN.yml +1 -1
- data/locales/zh-HK.yml +1 -1
- data/locales/zh-TW.yml +1 -1
- metadata +16 -16
- data/lib/pagy/extras/foundation.rb +0 -95
- data/lib/pagy/extras/items.rb +0 -64
- data/lib/pagy/extras/materialize.rb +0 -100
- data/lib/pagy/extras/semantic.rb +0 -94
- data/lib/pagy/extras/uikit.rb +0 -98
data/lib/pagy/extras/pagy.rb
CHANGED
@@ -12,7 +12,7 @@ class Pagy # :nodoc:
|
|
12
12
|
def pagy_nav_js(pagy, id: nil, aria_label: nil, **vars)
|
13
13
|
sequels = pagy.sequels(**vars)
|
14
14
|
id = %( id="#{id}") if id
|
15
|
-
a = pagy_anchor(pagy)
|
15
|
+
a = pagy_anchor(pagy, **vars)
|
16
16
|
tokens = { 'before' => prev_a(pagy, a),
|
17
17
|
'a' => a.(PAGE_TOKEN, LABEL_TOKEN),
|
18
18
|
'current' => %(<a class="current" role="link" aria-current="page" aria-disabled="true">#{
|
@@ -27,9 +27,9 @@ class Pagy # :nodoc:
|
|
27
27
|
end
|
28
28
|
|
29
29
|
# Javascript combo pagination: it returns a nav with a data-pagy attribute used by the pagy.js file
|
30
|
-
def pagy_combo_nav_js(pagy, id: nil, aria_label: nil)
|
30
|
+
def pagy_combo_nav_js(pagy, id: nil, aria_label: nil, **vars)
|
31
31
|
id = %( id="#{id}") if id
|
32
|
-
a = pagy_anchor(pagy)
|
32
|
+
a = pagy_anchor(pagy, **vars)
|
33
33
|
pages = pagy.pages
|
34
34
|
|
35
35
|
page_input = %(<input name="page" type="number" min="1" max="#{pages}" value="#{pagy.page}" aria-current="page" ) <<
|
@@ -37,7 +37,7 @@ class Pagy # :nodoc:
|
|
37
37
|
|
38
38
|
%(<nav#{id} class="pagy combo-nav-js" #{
|
39
39
|
nav_aria_label(pagy, aria_label:)} #{
|
40
|
-
pagy_data(pagy, :combo, pagy_url_for(pagy, PAGE_TOKEN))}>#{
|
40
|
+
pagy_data(pagy, :combo, pagy_url_for(pagy, PAGE_TOKEN, **vars))}>#{
|
41
41
|
prev_a(pagy, a)
|
42
42
|
}<label>#{
|
43
43
|
pagy_t('pagy.combo_nav_js', page_input:, pages:)
|
@@ -47,35 +47,35 @@ class Pagy # :nodoc:
|
|
47
47
|
end
|
48
48
|
|
49
49
|
# Return the previous page URL string or nil
|
50
|
-
def pagy_prev_url(pagy,
|
51
|
-
pagy_url_for(pagy, pagy.prev,
|
50
|
+
def pagy_prev_url(pagy, **vars)
|
51
|
+
pagy_url_for(pagy, pagy.prev, **vars) if pagy.prev
|
52
52
|
end
|
53
53
|
|
54
54
|
# Return the next page URL string or nil
|
55
|
-
def pagy_next_url(pagy,
|
56
|
-
pagy_url_for(pagy, pagy.next,
|
55
|
+
def pagy_next_url(pagy, **vars)
|
56
|
+
pagy_url_for(pagy, pagy.next, **vars) if pagy.next
|
57
57
|
end
|
58
58
|
|
59
59
|
# Return the enabled/disabled previous page anchor tag
|
60
|
-
def pagy_prev_a(pagy, text: pagy_t('pagy.prev'), aria_label: pagy_t('pagy.aria_label.prev'))
|
61
|
-
a = pagy_anchor(pagy)
|
60
|
+
def pagy_prev_a(pagy, text: pagy_t('pagy.prev'), aria_label: pagy_t('pagy.aria_label.prev'), **vars)
|
61
|
+
a = pagy_anchor(pagy, **vars)
|
62
62
|
prev_a(pagy, a, text:, aria_label:)
|
63
63
|
end
|
64
64
|
|
65
65
|
# Return the enabled/disabled next page anchor tag
|
66
|
-
def pagy_next_a(pagy, text: pagy_t('pagy.next'), aria_label: pagy_t('pagy.aria_label.next'))
|
67
|
-
a = pagy_anchor(pagy)
|
66
|
+
def pagy_next_a(pagy, text: pagy_t('pagy.next'), aria_label: pagy_t('pagy.aria_label.next'), **vars)
|
67
|
+
a = pagy_anchor(pagy, **vars)
|
68
68
|
next_a(pagy, a, text:, aria_label:)
|
69
69
|
end
|
70
70
|
|
71
71
|
# Conditionally return the previous page link tag
|
72
|
-
def pagy_prev_link(pagy,
|
73
|
-
%(<link href="#{pagy_url_for(pagy, pagy.prev,
|
72
|
+
def pagy_prev_link(pagy, **vars)
|
73
|
+
%(<link href="#{pagy_url_for(pagy, pagy.prev, **vars)}"/>) if pagy.prev
|
74
74
|
end
|
75
75
|
|
76
76
|
# Conditionally return the next page link tag
|
77
|
-
def pagy_next_link(pagy,
|
78
|
-
%(<link href="#{pagy_url_for(pagy, pagy.next,
|
77
|
+
def pagy_next_link(pagy, **vars)
|
78
|
+
%(<link href="#{pagy_url_for(pagy, pagy.next, **vars)}"/>) if pagy.next
|
79
79
|
end
|
80
80
|
end
|
81
81
|
Frontend.prepend PagyExtra
|
@@ -23,11 +23,11 @@ class Pagy # :nodoc:
|
|
23
23
|
# Additions for the Pagy class
|
24
24
|
module PagyExtension
|
25
25
|
# Create a Pagy object from a Searchkick::Results object
|
26
|
-
def new_from_searchkick(results, vars
|
27
|
-
vars[:
|
26
|
+
def new_from_searchkick(results, **vars)
|
27
|
+
vars[:limit] = results.options[:per_page]
|
28
28
|
vars[:page] = results.options[:page]
|
29
29
|
vars[:count] = results.total_count
|
30
|
-
new(vars)
|
30
|
+
new(**vars)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
Pagy.extend PagyExtension
|
@@ -37,17 +37,17 @@ class Pagy # :nodoc:
|
|
37
37
|
private
|
38
38
|
|
39
39
|
# Return Pagy object and results
|
40
|
-
def pagy_searchkick(pagy_search_args, vars
|
40
|
+
def pagy_searchkick(pagy_search_args, **vars)
|
41
41
|
model, term, options, block, *called = pagy_search_args
|
42
42
|
vars = pagy_searchkick_get_vars(nil, vars)
|
43
|
-
options[:per_page] = vars[:
|
43
|
+
options[:per_page] = vars[:limit]
|
44
44
|
options[:page] = vars[:page]
|
45
45
|
results = model.send(DEFAULT[:searchkick_search], term, **options, &block)
|
46
46
|
vars[:count] = results.total_count
|
47
47
|
|
48
|
-
pagy = ::Pagy.new(vars)
|
48
|
+
pagy = ::Pagy.new(**vars)
|
49
49
|
# with :last_page overflow we need to re-run the method in order to get the hits
|
50
|
-
return pagy_searchkick(pagy_search_args, vars
|
50
|
+
return pagy_searchkick(pagy_search_args, **vars, page: pagy.page) \
|
51
51
|
if defined?(::Pagy::OverflowExtra) && pagy.overflow? && pagy.vars[:overflow] == :last_page
|
52
52
|
|
53
53
|
[pagy, called.empty? ? results : results.send(*called)]
|
@@ -56,10 +56,10 @@ class Pagy # :nodoc:
|
|
56
56
|
# Sub-method called only by #pagy_searchkick: here for easy customization of variables by overriding
|
57
57
|
# the _collection argument is not available when the method is called
|
58
58
|
def pagy_searchkick_get_vars(_collection, vars)
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
59
|
+
vars.tap do |v|
|
60
|
+
v[:page] ||= pagy_get_page(v)
|
61
|
+
v[:limit] ||= pagy_get_limit(v) || DEFAULT[:limit]
|
62
|
+
end
|
63
63
|
end
|
64
64
|
end
|
65
65
|
Backend.prepend BackendAddOn
|
data/lib/pagy/extras/size.rb
CHANGED
@@ -5,11 +5,11 @@ class Pagy # :nodoc:
|
|
5
5
|
# Implement the legacy bar using the array size.
|
6
6
|
# Unless you have very specific requirements, use the faster and better looking default bar.
|
7
7
|
module SizeExtra
|
8
|
-
#
|
8
|
+
# Implements the old series algorithm
|
9
9
|
def series(size: @vars[:size], **_)
|
10
10
|
return super unless size.is_a?(Array)
|
11
11
|
return [] if size == []
|
12
|
-
raise VariableError.new(self, :size, 'to be an Array of
|
12
|
+
raise VariableError.new(self, :size, 'to be an Array of 4 Integers or []', size) \
|
13
13
|
unless size.is_a?(Array) && size.size == 4 && size.all? { |num| !num.negative? rescue false } # rubocop:disable Style/RescueModifier
|
14
14
|
|
15
15
|
[].tap do |series|
|
@@ -12,10 +12,6 @@ class Pagy # :nodoc:
|
|
12
12
|
module QueryUtils
|
13
13
|
module_function
|
14
14
|
|
15
|
-
def escape(str)
|
16
|
-
URI.encode_www_form_component(str)
|
17
|
-
end
|
18
|
-
|
19
15
|
def build_nested_query(value, prefix = nil)
|
20
16
|
case value
|
21
17
|
when Array
|
@@ -32,13 +28,17 @@ class Pagy # :nodoc:
|
|
32
28
|
"#{escape(prefix)}=#{escape(value)}"
|
33
29
|
end
|
34
30
|
end
|
31
|
+
|
32
|
+
def escape(str)
|
33
|
+
URI.encode_www_form_component(str)
|
34
|
+
end
|
35
35
|
end
|
36
36
|
# :nocov:
|
37
37
|
|
38
38
|
# Return the URL for the page. If there is no pagy.vars[:url]
|
39
39
|
# it works exactly as the regular #pagy_url_for, relying on the params method and Rack.
|
40
40
|
# If there is a defined pagy.vars[:url] variable it does not need the params method nor Rack.
|
41
|
-
def pagy_url_for(pagy, page,
|
41
|
+
def pagy_url_for(pagy, page, fragment: nil, **_)
|
42
42
|
return super unless pagy.vars[:url]
|
43
43
|
|
44
44
|
vars = pagy.vars
|
@@ -46,7 +46,7 @@ class Pagy # :nodoc:
|
|
46
46
|
pagy_set_query_params(page, vars, params)
|
47
47
|
params = vars[:params].(params) if vars[:params].is_a?(Proc)
|
48
48
|
query_string = "?#{QueryUtils.build_nested_query(params)}"
|
49
|
-
"#{vars[:url]}#{query_string}#{
|
49
|
+
"#{vars[:url]}#{query_string}#{fragment}"
|
50
50
|
end
|
51
51
|
end
|
52
52
|
UrlHelpers.prepend StandaloneExtra
|
data/lib/pagy/extras/trim.rb
CHANGED
@@ -6,9 +6,9 @@ class Pagy # :nodoc:
|
|
6
6
|
|
7
7
|
# Remove the page=1 param from the first page link
|
8
8
|
module TrimExtra
|
9
|
-
# Override the original
|
9
|
+
# Override the original pagy_anchor.
|
10
10
|
# Call the pagy_trim method for page 1 if the trim_extra is enabled
|
11
|
-
def pagy_anchor(pagy)
|
11
|
+
def pagy_anchor(pagy, **_)
|
12
12
|
a_proc = super
|
13
13
|
return a_proc unless pagy.vars[:trim_extra]
|
14
14
|
|
data/lib/pagy/frontend.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# See Pagy::Frontend API documentation: https://ddnexus.github.io/pagy/docs/api/frontend
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require_relative 'url_helpers'
|
5
4
|
require_relative 'i18n'
|
5
|
+
require_relative 'url_helpers'
|
6
6
|
|
7
7
|
class Pagy
|
8
8
|
# Used for search and replace, hardcoded also in the pagy.js file
|
@@ -14,26 +14,17 @@ class Pagy
|
|
14
14
|
module Frontend
|
15
15
|
include UrlHelpers
|
16
16
|
|
17
|
-
#
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
a.(item)
|
28
|
-
when String
|
29
|
-
%(<a role="link" aria-disabled="true" aria-current="page" class="current">#{pagy.label_for(item)}</a>)
|
30
|
-
when :gap
|
31
|
-
%(<a role="link" aria-disabled="true" class="gap">#{pagy_t('pagy.gap')}</a>)
|
32
|
-
else
|
33
|
-
raise InternalError, "expected item types in series to be Integer, String or :gap; got #{item.inspect}"
|
34
|
-
end
|
17
|
+
# Return a performance optimized lambda to generate the HTML anchor element (a tag)
|
18
|
+
# Benchmarked on a 20 link nav: it is ~22x faster and uses ~18x less memory than rails' link_to
|
19
|
+
def pagy_anchor(pagy, anchor_string: nil, **vars)
|
20
|
+
anchor_string &&= %( #{anchor_string})
|
21
|
+
left, right = %(<a#{anchor_string} href="#{pagy_url_for(pagy, PAGE_TOKEN, **vars)}").split(PAGE_TOKEN, 2)
|
22
|
+
# lambda used by all the helpers
|
23
|
+
lambda do |page, text = pagy.label_for(page), classes: nil, aria_label: nil|
|
24
|
+
classes = %( class="#{classes}") if classes
|
25
|
+
aria_label = %( aria-label="#{aria_label}") if aria_label
|
26
|
+
%(#{left}#{page}#{right}#{classes}#{aria_label}>#{text}</a>)
|
35
27
|
end
|
36
|
-
html << %(#{next_a(pagy, a)}</nav>)
|
37
28
|
end
|
38
29
|
|
39
30
|
# Return examples: "Displaying items 41-60 of 324 in total" or "Displaying Products 41-60 of 324 in total"
|
@@ -54,24 +45,32 @@ class Pagy
|
|
54
45
|
}</span>)
|
55
46
|
end
|
56
47
|
|
57
|
-
#
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
48
|
+
# Generic pagination: it returns the html with the series of links to the pages
|
49
|
+
def pagy_nav(pagy, id: nil, aria_label: nil, anchor_string: nil, **vars)
|
50
|
+
id = %( id="#{id}") if id
|
51
|
+
a = pagy_anchor(pagy, anchor_string:)
|
52
|
+
|
53
|
+
html = %(<nav#{id} class="pagy nav" #{nav_aria_label(pagy, aria_label:)}>#{
|
54
|
+
prev_a(pagy, a)})
|
55
|
+
pagy.series(**vars).each do |item| # series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36]
|
56
|
+
html << case item
|
57
|
+
when Integer
|
58
|
+
a.(item)
|
59
|
+
when String
|
60
|
+
%(<a role="link" aria-disabled="true" aria-current="page" class="current">#{pagy.label_for(item)}</a>)
|
61
|
+
when :gap
|
62
|
+
%(<a role="link" aria-disabled="true" class="gap">#{pagy_t('pagy.gap')}</a>)
|
63
|
+
else
|
64
|
+
raise InternalError, "expected item types in series to be Integer, String or :gap; got #{item.inspect}"
|
65
|
+
end
|
68
66
|
end
|
67
|
+
html << %(#{next_a(pagy, a)}</nav>)
|
69
68
|
end
|
70
69
|
|
71
70
|
# Similar to I18n.t: just ~18x faster using ~10x less memory
|
72
71
|
# (@pagy_locale explicitly initialized in order to avoid warning)
|
73
|
-
def pagy_t(key, opts
|
74
|
-
Pagy::I18n.translate(@pagy_locale ||= nil, key, opts)
|
72
|
+
def pagy_t(key, **opts)
|
73
|
+
Pagy::I18n.translate(@pagy_locale ||= nil, key, **opts)
|
75
74
|
end
|
76
75
|
|
77
76
|
private
|
data/lib/pagy/i18n.rb
CHANGED
@@ -154,7 +154,7 @@ class Pagy
|
|
154
154
|
end
|
155
155
|
|
156
156
|
# Translate and pluralize the key with the locale DATA
|
157
|
-
def translate(locale, key, opts
|
157
|
+
def translate(locale, key, **opts)
|
158
158
|
data, pluralize = DATA[locale]
|
159
159
|
translation = data[key] || (opts[:count] && data[key += ".#{pluralize.call(opts[:count])}"]) \
|
160
160
|
or return %([translation missing: "#{key}"])
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# See Pagy API documentation: https://ddnexus.github.io/pagy/docs/api/keyset
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class Pagy
|
5
|
+
class Keyset
|
6
|
+
# Keyset adapter for ActiveRecord
|
7
|
+
class ActiveRecord < Keyset
|
8
|
+
protected
|
9
|
+
|
10
|
+
# Get the keyset attributes of the record
|
11
|
+
def latest_from(latest_record) = latest_record.slice(*@keyset.keys)
|
12
|
+
|
13
|
+
# Extract the keyset from the set
|
14
|
+
def extract_keyset
|
15
|
+
@set.order_values.each_with_object({}) do |node, keyset|
|
16
|
+
keyset[node.value.name.to_sym] = node.direction
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Filter out the already retrieved records
|
21
|
+
def after_latest = @set.where(after_latest_query, **@latest)
|
22
|
+
|
23
|
+
# Append the missing keyset keys if the set is restricted by select
|
24
|
+
def apply_select
|
25
|
+
@set.select(*@keyset.keys)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Set with selected columns?
|
29
|
+
def select? = !@set.select_values.empty?
|
30
|
+
|
31
|
+
# Typecast the latest attributes
|
32
|
+
def typecast_latest(latest)
|
33
|
+
@set.model.new(latest).slice(latest.keys)
|
34
|
+
.to_hash.transform_keys(&:to_sym)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# See Pagy API documentation: https://ddnexus.github.io/pagy/docs/api/keyset
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class Pagy
|
5
|
+
class Keyset
|
6
|
+
# Keyset adapter for sequel
|
7
|
+
class Sequel < Keyset
|
8
|
+
protected
|
9
|
+
|
10
|
+
# Get the keyset attributes of the latest record
|
11
|
+
def latest_from(latest_record) = latest_record.to_hash.slice(*@keyset.keys)
|
12
|
+
|
13
|
+
# Extract the keyset from the set
|
14
|
+
def extract_keyset
|
15
|
+
return {} unless @set.opts[:order]
|
16
|
+
|
17
|
+
@set.opts[:order].each_with_object({}) do |item, keyset|
|
18
|
+
case item
|
19
|
+
when Symbol
|
20
|
+
keyset[item] = :asc
|
21
|
+
when ::Sequel::SQL::OrderedExpression
|
22
|
+
keyset[item.expression] = item.descending ? :desc : :asc
|
23
|
+
else
|
24
|
+
raise TypeError, "#{item.class.inspect} is not a supported Sequel::SQL::OrderedExpression"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Filter out the already retrieved records
|
30
|
+
def after_latest = @set.where(::Sequel.lit(after_latest_query, **@latest))
|
31
|
+
|
32
|
+
# Append the missing keyset keys if the set is restricted by select
|
33
|
+
def apply_select
|
34
|
+
selected = @set.opts[:select]
|
35
|
+
@set.select_append(*@keyset.keys.reject { |c| selected.include?(c) })
|
36
|
+
end
|
37
|
+
|
38
|
+
# Set with selected columns?
|
39
|
+
def select? = !@set.opts[:select].nil?
|
40
|
+
|
41
|
+
# Typecast the latest attributes
|
42
|
+
def typecast_latest(latest)
|
43
|
+
model = @set.opts[:model]
|
44
|
+
model.unrestrict_primary_key if (restricted_pk = model.restrict_primary_key?)
|
45
|
+
latest = model.new(latest).to_hash.slice(*latest.keys.map(&:to_sym))
|
46
|
+
model.restrict_primary_key if restricted_pk
|
47
|
+
latest
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/pagy/keyset.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# See Pagy API documentation: https://ddnexus.github.io/pagy/docs/api/keyset
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require_relative 'b64'
|
6
|
+
require_relative 'shared_methods'
|
7
|
+
|
8
|
+
class Pagy
|
9
|
+
# Implement wicked-fast keyset pagination for big data
|
10
|
+
class Keyset
|
11
|
+
class TypeError < ::TypeError; end
|
12
|
+
|
13
|
+
include SharedMethods
|
14
|
+
|
15
|
+
# Pick the right adapter for the set
|
16
|
+
def self.new(set, **vars)
|
17
|
+
if self == Pagy::Keyset
|
18
|
+
if defined?(::ActiveRecord) && set.is_a?(::ActiveRecord::Relation)
|
19
|
+
ActiveRecord
|
20
|
+
elsif defined?(::Sequel) && set.is_a?(::Sequel::Dataset)
|
21
|
+
Sequel
|
22
|
+
else
|
23
|
+
raise TypeError, "expected set to be an instance of ActiveRecord::Relation or Sequel::Dataset; got #{set.class}"
|
24
|
+
end.new(set, **vars)
|
25
|
+
else
|
26
|
+
allocate.tap { |instance| instance.send(:initialize, set, **vars) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :latest # Other readers from SharedMethods
|
31
|
+
|
32
|
+
def initialize(set, **vars)
|
33
|
+
default = DEFAULT.slice(:limit, :page_param, # from pagy
|
34
|
+
:headers, # from headers extra
|
35
|
+
:jsonapi, # from jsonapi extra
|
36
|
+
:limit_param, :limit_max, :limit_extra) # from limit_extra
|
37
|
+
assign_vars({ **default, page: nil }, vars)
|
38
|
+
assign_limit
|
39
|
+
@set = set
|
40
|
+
@page = @vars[:page]
|
41
|
+
@keyset = extract_keyset
|
42
|
+
raise InternalError, 'the set must be ordered' if @keyset.empty?
|
43
|
+
return unless @page
|
44
|
+
|
45
|
+
latest = JSON.parse(B64.urlsafe_decode(@page)).transform_keys(&:to_sym)
|
46
|
+
@latest = @vars[:typecast_latest]&.(latest) || typecast_latest(latest)
|
47
|
+
raise InternalError, 'page and keyset are not consistent' \
|
48
|
+
unless @latest.keys == @keyset.keys
|
49
|
+
end
|
50
|
+
|
51
|
+
# Return the next page
|
52
|
+
def next
|
53
|
+
records
|
54
|
+
return unless @more
|
55
|
+
|
56
|
+
@next ||= B64.urlsafe_encode(latest_from(@records.last).to_json)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Retrieve the array of records for the current page
|
60
|
+
def records
|
61
|
+
@records ||= begin
|
62
|
+
@set = apply_select if select?
|
63
|
+
@set = @vars[:after_latest]&.(@set, @latest) || after_latest if @latest
|
64
|
+
records = @set.limit(@limit + 1).to_a
|
65
|
+
@more = records.size > @limit && !records.pop.nil?
|
66
|
+
records
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
# Prepare the literal query to filter out the already fetched records
|
73
|
+
def after_latest_query
|
74
|
+
operator = { asc: '>', desc: '<' }
|
75
|
+
directions = @keyset.values
|
76
|
+
if @vars[:tuple_comparison] && (directions.all?(:asc) || directions.all?(:desc))
|
77
|
+
columns = @keyset.keys
|
78
|
+
placeholders = columns.map { |column| ":#{column}" }.join(', ')
|
79
|
+
"( #{columns.join(', ')} ) #{operator[directions.first]} ( #{placeholders} )"
|
80
|
+
else
|
81
|
+
keyset = @keyset.to_a
|
82
|
+
where = []
|
83
|
+
until keyset.empty?
|
84
|
+
last_column, last_direction = keyset.pop
|
85
|
+
query = +'( '
|
86
|
+
query << (keyset.map { |column, _d| "#{column} = :#{column}" } \
|
87
|
+
<< "#{last_column} #{operator[last_direction]} :#{last_column}").join(' AND ')
|
88
|
+
query << ' )'
|
89
|
+
where << query
|
90
|
+
end
|
91
|
+
where.join(' OR ')
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
require_relative 'keyset/active_record'
|
98
|
+
require_relative 'keyset/sequel'
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Pagy
|
4
|
+
# Shared with Keyset
|
5
|
+
module SharedMethods
|
6
|
+
attr_reader :page, :limit, :vars
|
7
|
+
|
8
|
+
# Validates and assign the passed vars: var must be present and value.to_i must be >= to min
|
9
|
+
def assign_and_check(name_min)
|
10
|
+
name_min.each do |name, min|
|
11
|
+
raise VariableError.new(self, name, ">= #{min}", @vars[name]) \
|
12
|
+
unless @vars[name]&.respond_to?(:to_i) && \
|
13
|
+
instance_variable_set(:"@#{name}", @vars[name].to_i) >= min
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Assign @limit (overridden by the gearbox extra)
|
18
|
+
def assign_limit
|
19
|
+
assign_and_check(limit: 1)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Assign @vars
|
23
|
+
def assign_vars(default, vars)
|
24
|
+
@vars = { **default, **vars.delete_if { |k, v| default.key?(k) && (v.nil? || v == '') } }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/pagy/url_helpers.rb
CHANGED
@@ -6,21 +6,21 @@ class Pagy
|
|
6
6
|
# Return the URL for the page, relying on the params method and Rack by default.
|
7
7
|
# It supports all Rack-based frameworks (Sinatra, Padrino, Rails, ...).
|
8
8
|
# For non-rack environments you can use the standalone extra
|
9
|
-
def pagy_url_for(pagy, page, absolute: false, **_)
|
9
|
+
def pagy_url_for(pagy, page, absolute: false, fragment: nil, **_)
|
10
10
|
vars = pagy.vars
|
11
11
|
query_params = request.GET.clone
|
12
12
|
query_params.merge!(vars[:params].transform_keys(&:to_s)) if vars[:params].is_a?(Hash)
|
13
13
|
pagy_set_query_params(page, vars, query_params)
|
14
14
|
query_params = vars[:params].(query_params) if vars[:params].is_a?(Proc)
|
15
15
|
query_string = "?#{Rack::Utils.build_nested_query(query_params)}"
|
16
|
-
"#{request.base_url if absolute}#{vars[:request_path] || request.path}#{query_string}#{
|
16
|
+
"#{request.base_url if absolute}#{vars[:request_path] || request.path}#{query_string}#{fragment}"
|
17
17
|
end
|
18
18
|
|
19
|
-
# Add the page and
|
19
|
+
# Add the page and limit params
|
20
20
|
# Overridable by the jsonapi extra
|
21
21
|
def pagy_set_query_params(page, vars, query_params)
|
22
22
|
query_params[vars[:page_param].to_s] = page
|
23
|
-
query_params[vars[:
|
23
|
+
query_params[vars[:limit_param].to_s] = vars[:limit] if vars[:limit_extra]
|
24
24
|
end
|
25
25
|
end
|
26
26
|
end
|