pagy 8.6.0 → 9.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/apps/calendar.ru +5 -5
  3. data/apps/demo.ru +8 -9
  4. data/apps/keyset_ar.ru +236 -0
  5. data/apps/keyset_s.ru +238 -0
  6. data/apps/rails.ru +7 -9
  7. data/apps/repro.ru +5 -6
  8. data/apps/tmp/calendar.sqlite3 +0 -0
  9. data/apps/tmp/calendar.sqlite3-shm +0 -0
  10. data/apps/tmp/calendar.sqlite3-wal +0 -0
  11. data/apps/tmp/local_secret.txt +1 -1
  12. data/apps/tmp/pagy-keyset-ar.sqlite3 +0 -0
  13. data/apps/tmp/{pagy-rails.sqlite3 → pagy-keyset-ar.sqlite3-shm} +0 -0
  14. data/apps/tmp/pagy-keyset-ar.sqlite3-wal +0 -0
  15. data/apps/tmp/pagy-keyset-s.sqlite3 +0 -0
  16. data/bin/pagy +4 -2
  17. data/config/pagy.rb +34 -31
  18. data/javascripts/pagy-module.js +1 -1
  19. data/javascripts/pagy.js +2 -2
  20. data/javascripts/pagy.min.js +2 -2
  21. data/javascripts/pagy.min.js.map +3 -3
  22. data/javascripts/pagy.mjs +2 -2
  23. data/lib/pagy/b64.rb +33 -0
  24. data/lib/pagy/backend.rb +21 -17
  25. data/lib/pagy/calendar/day.rb +1 -1
  26. data/lib/pagy/calendar/month.rb +1 -1
  27. data/lib/pagy/calendar/quarter.rb +1 -1
  28. data/lib/pagy/calendar/unit.rb +7 -10
  29. data/lib/pagy/calendar/week.rb +1 -1
  30. data/lib/pagy/calendar/year.rb +1 -1
  31. data/lib/pagy/calendar.rb +5 -5
  32. data/lib/pagy/countless.rb +11 -15
  33. data/lib/pagy/extras/arel.rb +8 -10
  34. data/lib/pagy/extras/array.rb +4 -6
  35. data/lib/pagy/extras/bootstrap.rb +5 -5
  36. data/lib/pagy/extras/bulma.rb +10 -7
  37. data/lib/pagy/extras/calendar.rb +4 -5
  38. data/lib/pagy/extras/countless.rb +15 -13
  39. data/lib/pagy/extras/elasticsearch_rails.rb +15 -15
  40. data/lib/pagy/extras/gearbox.rb +26 -26
  41. data/lib/pagy/extras/headers.rb +25 -24
  42. data/lib/pagy/extras/js_tools.rb +8 -8
  43. data/lib/pagy/extras/jsonapi.rb +26 -16
  44. data/lib/pagy/extras/keyset.rb +36 -0
  45. data/lib/pagy/extras/limit.rb +63 -0
  46. data/lib/pagy/extras/meilisearch.rb +11 -11
  47. data/lib/pagy/extras/metadata.rb +2 -2
  48. data/lib/pagy/extras/overflow.rb +6 -6
  49. data/lib/pagy/extras/pagy.rb +16 -16
  50. data/lib/pagy/extras/searchkick.rb +11 -11
  51. data/lib/pagy/extras/size.rb +2 -2
  52. data/lib/pagy/extras/standalone.rb +6 -6
  53. data/lib/pagy/extras/trim.rb +2 -2
  54. data/lib/pagy/frontend.rb +32 -33
  55. data/lib/pagy/i18n.rb +1 -1
  56. data/lib/pagy/keyset/active_record.rb +38 -0
  57. data/lib/pagy/keyset/sequel.rb +51 -0
  58. data/lib/pagy/keyset.rb +98 -0
  59. data/lib/pagy/shared_methods.rb +27 -0
  60. data/lib/pagy/url_helpers.rb +4 -4
  61. data/lib/pagy.rb +54 -68
  62. data/locales/ar.yml +2 -1
  63. data/locales/be.yml +1 -1
  64. data/locales/bg.yml +1 -1
  65. data/locales/bs.yml +1 -1
  66. data/locales/ca.yml +1 -1
  67. data/locales/ckb.yml +1 -1
  68. data/locales/cs.yml +1 -1
  69. data/locales/da.yml +1 -1
  70. data/locales/de.yml +1 -1
  71. data/locales/en.yml +1 -1
  72. data/locales/es.yml +1 -1
  73. data/locales/fr.yml +1 -1
  74. data/locales/hr.yml +1 -1
  75. data/locales/id.yml +1 -1
  76. data/locales/it.yml +1 -1
  77. data/locales/ja.yml +1 -1
  78. data/locales/km.yml +1 -1
  79. data/locales/ko.yml +1 -1
  80. data/locales/nb.yml +1 -1
  81. data/locales/nl.yml +1 -1
  82. data/locales/nn.yml +1 -1
  83. data/locales/pl.yml +1 -1
  84. data/locales/pt-BR.yml +1 -1
  85. data/locales/pt.yml +1 -1
  86. data/locales/ru.yml +1 -1
  87. data/locales/sr.yml +1 -1
  88. data/locales/sv-SE.yml +1 -1
  89. data/locales/sv.yml +1 -1
  90. data/locales/sw.yml +1 -1
  91. data/locales/ta.yml +1 -1
  92. data/locales/tr.yml +1 -1
  93. data/locales/uk.yml +1 -1
  94. data/locales/vi.yml +1 -1
  95. data/locales/zh-CN.yml +1 -1
  96. data/locales/zh-HK.yml +1 -1
  97. data/locales/zh-TW.yml +1 -1
  98. metadata +16 -16
  99. data/lib/pagy/extras/foundation.rb +0 -95
  100. data/lib/pagy/extras/items.rb +0 -64
  101. data/lib/pagy/extras/materialize.rb +0 -100
  102. data/lib/pagy/extras/semantic.rb +0 -94
  103. data/lib/pagy/extras/uikit.rb +0 -98
@@ -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, absolute: false)
51
- pagy_url_for(pagy, pagy.prev, absolute:) if 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, absolute: false)
56
- pagy_url_for(pagy, pagy.next, absolute:) if 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, absolute: false)
73
- %(<link href="#{pagy_url_for(pagy, pagy.prev, absolute:)}"/>) if 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, absolute: false)
78
- %(<link href="#{pagy_url_for(pagy, pagy.next, absolute:)}"/>) if 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[:items] = results.options[:per_page]
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[:items]
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.merge(page: pagy.page)) \
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
- pagy_set_items_from_params(vars) if defined?(ItemsExtra)
60
- vars[:items] ||= DEFAULT[:items]
61
- vars[:page] ||= pagy_get_page(vars)
62
- vars
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
@@ -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
- # Setup @items based on the :gearbox_items variable
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 size 4 || 0', size) \
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, absolute: false, **_)
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}#{vars[:fragment]}"
49
+ "#{vars[:url]}#{query_string}#{fragment}"
50
50
  end
51
51
  end
52
52
  UrlHelpers.prepend StandaloneExtra
@@ -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 pagy_a_proc.
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
- # Generic pagination: it returns the html with the series of links to the pages
18
- def pagy_nav(pagy, id: nil, aria_label: nil, **vars)
19
- id = %( id="#{id}") if id
20
- a = pagy_anchor(pagy)
21
-
22
- html = %(<nav#{id} class="pagy nav" #{nav_aria_label(pagy, aria_label:)}>#{
23
- prev_a(pagy, a)})
24
- pagy.series(**vars).each do |item| # series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36]
25
- html << case item
26
- when Integer
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
- # Return a performance optimized lambda to generate the HTML anchor element (a tag)
58
- # Benchmarked on a 20 link nav: it is ~22x faster and uses ~18x less memory than rails' link_to
59
- def pagy_anchor(pagy)
60
- a_string = pagy.vars[:anchor_string]
61
- a_string = %( #{a_string}) if a_string
62
- left, right = %(<a#{a_string} href="#{pagy_url_for(pagy, PAGE_TOKEN)}").split(PAGE_TOKEN, 2)
63
- # lambda used by all the helpers
64
- lambda do |page, text = pagy.label_for(page), classes: nil, aria_label: nil|
65
- classes = %( class="#{classes}") if classes
66
- aria_label = %( aria-label="#{aria_label}") if aria_label
67
- %(#{left}#{page}#{right}#{classes}#{aria_label}>#{text}</a>)
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
@@ -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
@@ -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}#{vars[:fragment]}"
16
+ "#{request.base_url if absolute}#{vars[:request_path] || request.path}#{query_string}#{fragment}"
17
17
  end
18
18
 
19
- # Add the page and items params
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[:items_param].to_s] = vars[:items] if vars[:items_extra]
23
+ query_params[vars[:limit_param].to_s] = vars[:limit] if vars[:limit_extra]
24
24
  end
25
25
  end
26
26
  end