rb_pager 0.1.1 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08d892ce787fddafc76730efd1e2712da4424f483cfb93d17ec217e70530cefc'
4
- data.tar.gz: 1cbe5622d6f768047495fcc4a86eb87aabcab41cfc835a6e312d4c63acff9114
3
+ metadata.gz: bdd82410767649de7ec56d84ba2c311a08555eb4e53ca47e0974f8e54540cf02
4
+ data.tar.gz: fcc26daca9475701f37e4e5f259e8921edb2fd961ce64e874df1cf3cca84e33a
5
5
  SHA512:
6
- metadata.gz: abf2b7864075d88dc03fc3e615033cb428ca773e972338013199f0bf0490dbb5d6c8e8b95917b910b522499339c240756324ffab15a1475f8b6b5d1bf9a7d79f
7
- data.tar.gz: 2995f411fa986b57b746ffee5c5148a3fab4241cdd22e0f6b09ba60e575a1e5179244c577e2ee44289f1cc7f1defc1b76b0914e2814ad4f476145cce673af782
6
+ metadata.gz: 335ff86d22e237f2f14826edaf9357b220e0b48019dc9f0534b7f611370ae7f15fdff6956da2a552c25a9fe316de7b5215079a690485c95a08171dd6d633a9d3
7
+ data.tar.gz: 12bc348e3abd3f82bfaee15fbd52a8ea15ca4ab9aca9da92cc0e3ba9e3345cb205bda27e4e1b9b05ae44fae84c330a77e978d2e3273027dec25e5eed4089720c
@@ -18,6 +18,16 @@ jobs:
18
18
 
19
19
  runs-on: ubuntu-latest
20
20
 
21
+ services:
22
+ db:
23
+ image: postgres:11.6-alpine
24
+ env:
25
+ POSTGRES_PASSWORD: password
26
+ POSTGRES_DB: rb_pager_test
27
+ ports:
28
+ - 5432:5432
29
+ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
30
+
21
31
  steps:
22
32
  - uses: actions/checkout@v2
23
33
  - name: Set up Ruby
@@ -29,8 +39,12 @@ jobs:
29
39
  ruby-version: 2.6
30
40
  - name: Setup System
31
41
  run: |
32
- sudo apt-get install libsqlite3-dev
42
+ sudo apt-get -y install libpq-dev
33
43
  - name: Install dependencies
34
44
  run: bundle install
35
45
  - name: Run tests
36
- run: bundle exec rake
46
+ env:
47
+ DATABASE_URL: postgres://postgres:password@localhost:5432/rb_pager_test
48
+ run: |
49
+ bundle exec rake db:create
50
+ bundle exec rspec
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rb_pager (0.1.0)
4
+ rb_pager (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -17,16 +17,12 @@ GEM
17
17
  minitest (~> 5.1)
18
18
  tzinfo (~> 1.1)
19
19
  zeitwerk (~> 2.2, >= 2.2.2)
20
- coderay (1.1.3)
21
20
  concurrent-ruby (1.1.6)
22
21
  diff-lcs (1.4.4)
23
22
  i18n (1.8.3)
24
23
  concurrent-ruby (~> 1.0)
25
- method_source (1.0.0)
26
24
  minitest (5.14.1)
27
- pry (0.13.1)
28
- coderay (~> 1.1)
29
- method_source (~> 1.0)
25
+ pg (1.2.3)
30
26
  rake (13.0.1)
31
27
  rspec (3.9.0)
32
28
  rspec-core (~> 3.9.0)
@@ -41,7 +37,6 @@ GEM
41
37
  diff-lcs (>= 1.2.0, < 2.0)
42
38
  rspec-support (~> 3.9.0)
43
39
  rspec-support (3.9.3)
44
- sqlite3 (1.4.2)
45
40
  thread_safe (0.3.6)
46
41
  tzinfo (1.2.7)
47
42
  thread_safe (~> 0.1)
@@ -52,11 +47,10 @@ PLATFORMS
52
47
 
53
48
  DEPENDENCIES
54
49
  activerecord (>= 5.2)
55
- pry
50
+ pg
56
51
  rake (~> 13.0)
57
52
  rb_pager!
58
53
  rspec (~> 3.0)
59
- sqlite3
60
54
 
61
55
  BUNDLED WITH
62
56
  2.1.4
data/README.md CHANGED
@@ -38,24 +38,37 @@ The system use `Base64.strict_decode64` and `Base64.strict_encode64` for cursor
38
38
 
39
39
  ```ruby
40
40
  # return first 20 employee records, by default limit set to 20
41
- Employee.pager(after: nil)
41
+ Employee.pager
42
42
 
43
43
  # return first 15 employee records
44
- Employee.pager(after: nil, limit: 15)
44
+ Employee.pager(limit: 15)
45
45
 
46
46
  # return first 10 employee records order by created_at asc
47
47
  # /employee?limit=10&sort=created_at
48
- Employee.pager(after: nil, limit: 10, sort: 'created_at')
48
+ Employee.pager(after: 'w33t44==', limit: 10, sort: 'created_at')
49
49
 
50
50
  # return first 10 employee records order by created_at desc
51
51
  # /employee?limit=10&sort=-created_at
52
- Employee.pager(after: nil, limit: 10, sort: '-created_at')
52
+ Employee.pager(after: 'w33t44==', limit: 10, sort: '-created_at')
53
53
 
54
54
  # [order by non uniq column](https://engineering.shopify.com/blogs/engineering/pagination-relative-cursors)
55
55
  # /employee?limit=10&sort=name,id
56
- Employee.pager(after: nil, limit: 10, sort: 'name,id')
56
+ Employee.pager(after: 'w33t44==', limit: 10, sort: 'name,id')
57
+
58
+ # on EmployeesController
59
+ records, meta = Employee.pager(limit: 15)
60
+ # meta => { next_cursor => 'uahOI==' }
61
+ render json: EmployeeSerializer.new(records, meta: meta).serialized_json, status: :ok
57
62
  ```
58
63
 
64
+ ## On progress
65
+
66
+ - [ ] implement `before` cursor
67
+ - [ ] meta url for first, last, next and previous page, collection size
68
+ - [ ] [error-cases](https://jsonapi.org/profiles/ethanresnick/cursor-pagination/#auto-id-error-cases)
69
+ - [ ] custom encoder
70
+ - [ ] ui helper
71
+
59
72
  ## Development
60
73
 
61
74
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/Rakefile CHANGED
@@ -4,3 +4,15 @@ require "rspec/core/rake_task"
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
6
  task :default => :spec
7
+
8
+ namespace :db do
9
+ desc "Create the test DB"
10
+ task :create do
11
+ `createdb rb_pager_test`
12
+ end
13
+
14
+ desc "Drop the test DB"
15
+ task :drop do
16
+ `dropdb rb_pager_test`
17
+ end
18
+ end
@@ -1,7 +1,6 @@
1
1
  require 'active_record'
2
2
  require 'rb_pager/version'
3
3
  require 'rb_pager/configuration'
4
- require 'pry'
5
4
 
6
5
  module RbPager
7
6
  class InvalidLimitValueError < StandardError; end
@@ -4,99 +4,124 @@ module RbPager
4
4
 
5
5
  module ClassMethods
6
6
  AR_ORDER = { '+' => :asc, '-' => :desc }
7
- AREL_ORDER = { asc: :gt, desc: :lt }
8
-
9
- def pager(after: nil, limit: nil, sort: nil)
10
- page_limit = limit || RbPager.configuration.limit
11
- sort_params = sort
12
7
 
8
+ def pager(after: nil, before: nil, limit: nil, sort: nil)
13
9
  raise InvalidLimitValueError if limit && limit < 1
10
+ instance_variable_set(:@sorted_columns, nil)
14
11
 
15
- sorted_columns, sorter = build_order_expression(sort)
16
- collection = if after.nil?
17
- order(sorter).extending(ActiveRecordRelationMethods).limit(page_limit)
18
- else
19
- custom_expression = create_custom_expression(after, sorted_columns)
20
- where(custom_expression).order(sorter).extending(ActiveRecordRelationMethods).limit(page_limit)
21
- end
22
-
23
- create_paginate_meta(collection, sorted_columns)
12
+ page_limit = limit || RbPager.configuration.limit
13
+ @sort = sort
14
+ @after = decode(after)
15
+ @before = decode(before)
16
+ @direction = :next
17
+
18
+ collection = where(apply_after)
19
+ collection = collection.where(apply_before)
20
+ collection = collection.order(sorted_columns)
21
+ .extending(ActiveRecordRelationMethods)
22
+ .limit(page_limit)
23
+
24
+ create_paginate_meta(collection)
24
25
  end
25
26
 
26
27
  private
27
28
 
28
- def create_custom_expression(cursor_params, sorted_columns)
29
- decode_cursor_params = JSON.parse(Base64.strict_decode64(cursor_params))
30
- return arel_table[primary_key].gt(decode_cursor_params[primary_key]) if sorted_columns.blank?
29
+ def decode(cursor)
30
+ return nil if cursor.nil?
31
31
 
32
- filter_ordered_columns = filter_with_ordered_columns(decode_cursor_params, sorted_columns)
33
- filter_primary_key = filter_with_primary_key(decode_cursor_params)
34
-
35
- filter_ordered_columns.or(filter_primary_key)
32
+ decode = Base64.strict_decode64(cursor)
33
+ Hash[
34
+ decode.split(',').map do |pair|
35
+ k, v = pair.split(':', 2)
36
+ end
37
+ ]
36
38
  end
37
39
 
38
- def filter_with_ordered_columns(decode_cursor_params, sorted_columns)
39
- result = self
40
- sorted_columns.each_with_index do |(column, type), index|
41
- result = if index.zero?
42
- result.arel_table[column].send(AREL_ORDER[type])
43
- else
44
- result.or(arel_table[column].send(AREL_ORDER[type]))
45
- end
40
+ def apply_after
41
+ return nil if @after.nil?
42
+
43
+ if sorted_columns.values.all? :asc
44
+ Arel::Nodes::GreaterThan.new(
45
+ Arel::Nodes::Grouping.new(@after.keys.map{ |col| arel_table[col] }),
46
+ Arel::Nodes::Grouping.new(@after.values.map{ |col| Arel::Nodes.build_quoted(col) })
47
+ )
48
+ else
49
+ @direction = :prev
50
+ Arel::Nodes::LessThan.new(
51
+ Arel::Nodes::Grouping.new(@after.keys.map{|col| arel_table[col]}),
52
+ Arel::Nodes::Grouping.new(@after.values.map{ |col| Arel::Nodes.build_quoted(col) })
53
+ )
46
54
  end
47
-
48
- result
49
55
  end
50
56
 
51
- def filter_with_primary_key(decode_cursor_params)
52
- result = self
53
-
54
- decode_cursor_params.each_with_index do |(column, value), index|
55
- result = if index.zero?
56
- result.arel_table[column].gt(value)
57
- else
58
- result.and(arel_table[column].eq(value))
59
- end
57
+ def apply_before
58
+ return nil if @before.nil?
59
+
60
+ if sorted_columns.values.all? :asc
61
+ @direction = :prev
62
+ Arel::Nodes::LessThan.new(
63
+ Arel::Nodes::Grouping.new(@before.keys.map{ |col| arel_table[col] }),
64
+ Arel::Nodes::Grouping.new(@before.values.map{ |col| Arel::Nodes.build_quoted(col) })
65
+ )
66
+ else
67
+ Arel::Nodes::GreaterThan.new(
68
+ Arel::Nodes::Grouping.new(@before.keys.map{|col| arel_table[col]}),
69
+ Arel::Nodes::Grouping.new(@before.values.map{ |col| Arel::Nodes.build_quoted(col) })
70
+ )
60
71
  end
61
-
62
- result
63
72
  end
64
73
 
65
- def build_order_expression(sort_params)
66
- return {} if sort_params.nil?
74
+ def sorted_columns
75
+ @sorted_columns ||= begin
76
+ sorted_columns = {} if @sort.nil?
77
+ sorted_columns ||= construct_sorted_columns
78
+ end
79
+ end
67
80
 
68
- sort_order = { '+' => 'ASC', '-' => 'DESC' }
81
+ def construct_sorted_columns
69
82
  sorted_params = {}
70
- arel_orders = []
71
-
72
- sort_params.split(',').each do |field|
73
- next unless attribute_names.include?(field)
83
+ fields = @sort.split(',')
74
84
 
85
+ fields.each do |field|
75
86
  sort_sign = field =~ /\A[+-]/ ? field.slice!(0) : '+'
76
- arel_orders << arel_table[field].send(AR_ORDER[sort_sign])
77
- sorted_params[field] = AR_ORDER[sort_sign]
87
+ sorted_params[field] = AR_ORDER[sort_sign] if attribute_names.include?(field)
78
88
  end
79
89
 
80
- [sorted_params, arel_orders]
90
+ sorted_params
81
91
  end
82
92
 
83
- def create_paginate_meta(collection, sorted_columns)
84
- next_cursor = next_cursor(collection, sorted_columns)
93
+ def create_paginate_meta(collection)
94
+ cursor = cursor(collection)
85
95
 
86
- meta = { next_cursor: next_cursor }
96
+ meta = { prev_cursor: cursor.first, next_cursor: cursor.last }
87
97
  [collection, meta]
88
98
  end
89
99
 
90
- def next_cursor(collection, sorted_columns)
91
- return '' unless collection.left_over?
100
+ def cursor(collection)
101
+ return ['', ''] if collection.total_size.zero?
92
102
 
93
- next_cursor = { 'id': collection.last.id }
103
+ prev_cursor, next_cursor = [], []
94
104
 
95
- sorted_columns.each do |key, _value|
96
- next_cursor.merge!(Hash[key, collection.last.send(key)])
105
+ if sorted_columns.blank?
106
+ prev_cursor = ["#{primary_key}:#{collection.first.send(primary_key)}"]
107
+ next_cursor = ["#{primary_key}:#{collection.last.send(primary_key)}"]
108
+ else
109
+ sorted_columns.each do |key, _value|
110
+ if type_for_attribute(key).type.eql? :datetime
111
+ prev_cursor << "#{key}:#{collection.first.send(key).rfc3339(9)}"
112
+ next_cursor << "#{key}:#{collection.last.send(key).rfc3339(9)}"
113
+ next
114
+ end
115
+
116
+ prev_cursor << "#{key}:#{collection.first.send(key)}"
117
+ next_cursor << "#{key}:#{collection.last.send(key)}"
118
+ end
97
119
  end
98
120
 
99
- Base64.strict_encode64(next_cursor.to_json)
121
+ return ['', Base64.strict_encode64(next_cursor.join(','))] if (@after.nil? && @before.nil?) || @direction.eql?(:prev) && !collection.left_over?
122
+ return [Base64.strict_encode64(prev_cursor.join(',')), ''] if @direction.eql?(:next) && !collection.left_over?
123
+
124
+ [Base64.strict_encode64(prev_cursor.join(',')), Base64.strict_encode64(next_cursor.join(','))]
100
125
  end
101
126
  end
102
127
 
@@ -1,3 +1,3 @@
1
1
  module RbPager
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -24,8 +24,7 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency "activerecord", ">= 5.2"
25
25
  spec.add_development_dependency "rake", "~> 13.0"
26
26
  spec.add_development_dependency "rspec", "~> 3.0"
27
- spec.add_development_dependency "sqlite3"
28
- spec.add_development_dependency "pry"
27
+ spec.add_development_dependency "pg"
29
28
 
30
29
  # Specify which files should be added to the gem when it is released.
31
30
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rb_pager
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - BambangSinaga
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-07-07 00:00:00.000000000 Z
11
+ date: 2020-07-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -53,21 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: sqlite3
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: pry
56
+ name: pg
71
57
  requirement: !ruby/object:Gem::Requirement
72
58
  requirements:
73
59
  - - ">="