api_cursor_pagination 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 70156ccecccc0ef4276361c8a58d3f701a3ab0d4fe83a4adbb443e075f1a969b
4
+ data.tar.gz: d81321a10c68c5417676cc785d647ffe3ab1e75c2468ccbaf34675991649c533
5
+ SHA512:
6
+ metadata.gz: 538461c6cc31fd905b810f4cf780fba10dbcd35aa814db54882413f972a8002260d41c680798f3dca69e5a57a5fd741df49a685e468455e2e24eeadc2d25576b
7
+ data.tar.gz: 27ee1766643580f2bd225bd7a04ca53898af70b079f7f663b1f94004e40cbacdd4cca06b2fee5ed30b39cb85d9b6803d118ba9259bf189ac0bf95eba8bf66721
data/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.0.0] - 2024-01-XX
11
+
12
+ ### Added
13
+ - Initial release of ApiCursorPagination gem
14
+ - Cursor-based pagination concern for Rails controllers
15
+ - JSON:API compliant pagination following the cursor pagination profile
16
+ - Support for `page[size]`, `page[before]`, and `page[after]` parameters
17
+ - Comprehensive error handling and validation
18
+ - Pagination metadata generation with `meta` and `links`
19
+ - Support for custom cursor fields and complex queries
20
+ - RSpec test suite with comprehensive coverage
21
+ - Documentation and usage examples
22
+ - Ruby >= 2.5 and Rails >= 5.0 compatibility
23
+
24
+ ### Features
25
+ - `validate_and_setup_page_params` method for page request parameter validation
26
+ - `paginate` method for retrieving paginated results
27
+ - `page_links_and_meta_data` method for generating pagination metadata
28
+ - Proper error responses for invalid parameters
29
+ - Support for bidirectional pagination (before/after cursors)
30
+ - Total count and page calculation
31
+ - Automatic link generation for next/previous pages
32
+
33
+ [Unreleased]: https://github.com/yourusername/api_cursor_pagination/compare/v1.0.0...HEAD
34
+ [1.0.0]: https://github.com/yourusername/api_cursor_pagination/releases/tag/v1.0.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 prashm
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 ApiCursorPagination Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,320 @@
1
+ # ApiCursorPagination
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/api_cursor_pagination.svg)](https://badge.fury.io/rb/api_cursor_pagination)
4
+ [![Build Status](https://github.com/prashm/api_cursor_pagination/workflows/CI/badge.svg)](https://github.com/prashm/api_cursor_pagination/actions)
5
+
6
+ A Rails concern that implements cursor-based pagination for APIs, following the [JSON:API cursor pagination profile](https://jsonapi.org/profiles/ethanresnick/cursor-pagination/).
7
+
8
+ ## Features
9
+
10
+ - 🚀 **Efficient Pagination**: Cursor-based pagination for large datasets
11
+ - 📊 **JSON:API Compliant**: Follows the JSON:API cursor pagination specification
12
+ - 🔧 **Easy Integration**: Simple Rails concern that can be included in any controller
13
+ - 🎯 **Flexible**: Supports custom cursor fields and query scopes
14
+ - ✅ **Well Tested**: Comprehensive test suite included
15
+ - 🛡️ **Error Handling**: Built-in validation and error responses
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ ```ruby
22
+ gem 'api_cursor_pagination'
23
+ ```
24
+
25
+ And then execute:
26
+
27
+ ```bash
28
+ $ bundle install
29
+ ```
30
+
31
+ Or install it yourself as:
32
+
33
+ ```bash
34
+ $ gem install api_cursor_pagination
35
+ ```
36
+
37
+ Or simply include the concern file (lib/api_cursor_pagination/concern.rb) in your app/controllers/concerns folder.
38
+
39
+
40
+ ## Requirements
41
+
42
+ - Ruby >= 2.5.0
43
+ - Rails >= 5.0
44
+ - ActiveSupport >= 5.0
45
+
46
+ ## Usage
47
+
48
+ ### Basic Setup
49
+
50
+ Include the concern in your API controller:
51
+
52
+ ```ruby
53
+ class UsersController < ApplicationController
54
+ include ApiCursorPagination::Concern
55
+
56
+ def index
57
+ # Initialize errors array
58
+ @errors = []
59
+
60
+ # Validate and set pagination options from params
61
+ validate_and_setup_page_params(params[:page])
62
+
63
+ if @errors.blank?
64
+ # Build your query scope
65
+ scope = User.active.includes(:profile).select('users.*, profiles.*, users.id user_id')
66
+
67
+ # Get paginated results
68
+ users = paginate(scope, 'users_id')
69
+
70
+ # Build API response with pagination metadata
71
+ response = {
72
+ status: 'Success',
73
+ data: users.map(&:as_json)
74
+ }.merge(page_links_and_meta_data(request.base_url + request.path, request.query_parameters))
75
+
76
+ render json: response, status: :ok
77
+ else
78
+ render json: error_response, status: :bad_request
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def error_response
85
+ {
86
+ status: 'Error',
87
+ errors: @errors.map { |error| error.is_a?(Hash) ? error : { title: error } }
88
+ }
89
+ end
90
+ end
91
+ ```
92
+
93
+ ### API Usage
94
+
95
+ #### Request Format
96
+
97
+ Pagination parameters should be passed in the `page` parameter:
98
+
99
+ ```
100
+ GET /api/users?page[size]=10
101
+ GET /api/users?page[size]=10&page[after]=123
102
+ GET /api/users?page[size]=10&page[before]=456
103
+ ```
104
+
105
+ #### Parameters
106
+
107
+ - `page[size]` (required): Number of records per page (must be positive integer)
108
+ - `page[after]` (optional): Cursor to get records after this ID
109
+ - `page[before]` (optional): Cursor to get records before this ID
110
+ - `page[sort]` (not supported): Will return an error if provided
111
+
112
+ **Note**: You cannot use both `page[before]` and `page[after]` in the same request.
113
+
114
+ #### Response Format
115
+
116
+ ```json
117
+ {
118
+ "status": "Success",
119
+ "data": [...],
120
+ "meta": {
121
+ "page": {
122
+ "cursor": {
123
+ "before": 123,
124
+ "after": 456
125
+ },
126
+ "total": 1000,
127
+ "pages": 100
128
+ }
129
+ },
130
+ "links": {
131
+ "prev": "https://api.example.com/users?page[before]=123&page[size]=10",
132
+ "next": "https://api.example.com/users?page[after]=456&page[size]=10"
133
+ }
134
+ }
135
+ ```
136
+
137
+ ### Advanced Usage
138
+
139
+ #### Custom Cursor Fields
140
+
141
+ You can use different fields for the SQL query scope and the returned row IDs:
142
+
143
+ ```ruby
144
+ # If your query uses a complex cursor but rows have simple IDs
145
+ scope = User.joins(:orders).select('users.*, CAST(CONCAT(users.id, ".", IFNULL(orders.id, 0)) AS DECIMAL(40,20)) as cursor_id')
146
+ users = paginate(scope, 'cursor_id')
147
+ ```
148
+
149
+ #### Complex Queries
150
+
151
+ The concern works with any ActiveRecord scope:
152
+
153
+ ```ruby
154
+ def index
155
+ validate_and_setup_page_params(params[:page])
156
+
157
+ if @errors.blank?
158
+ scope = User.joins(:orders)
159
+ .where(orders: { status: 'active' })
160
+ .group('users.id')
161
+ .select('users.*, MAX(orders.created_at) as last_order_date')
162
+
163
+ users = paginate(scope, 'last_order_date')
164
+
165
+ # ... render response
166
+ end
167
+ end
168
+ ```
169
+
170
+ ## API Reference
171
+
172
+ ### Methods
173
+
174
+ #### `validate_and_setup_page_params(params)`
175
+
176
+ Validates pagination parameters and sets instance variables.
177
+
178
+ **Parameters:**
179
+ - `params` - The request parameters hash
180
+
181
+ **Sets:**
182
+ - `@page_size` - Number of records per page
183
+ - `@page_before` - Cursor for pagination before this ID
184
+ - `@page_after` - Cursor for pagination after this ID
185
+ - `@errors` - Array of validation errors
186
+
187
+ #### `paginate(scope, scope_id_str, row_id_str = scope_id_str)`
188
+
189
+ Returns paginated results from the given scope.
190
+
191
+ **Parameters:**
192
+ - `scope` - ActiveRecord scope/relation
193
+ - `scope_id_str` - Field name used in SQL queries for cursor comparison
194
+ - `row_id_str` - Field name on returned objects for cursor values (defaults to scope_id_str)
195
+
196
+ **Returns:** Array of records
197
+
198
+ **Sets:**
199
+ - `@total_size` - Total number of records in scope
200
+ - `@total_pages` - Total number of pages
201
+ - `@next_page_cursor_id` - Cursor ID for next page
202
+ - `@prev_page_cursor_id` - Cursor ID for previous page
203
+
204
+ #### `page_links_and_meta_data(base_url, query_params)`
205
+
206
+ Generates pagination metadata and links.
207
+
208
+ **Parameters:**
209
+ - `base_url` - Base URL for pagination links
210
+ - `query_params` - Current query parameters
211
+
212
+ **Returns:** Hash with `meta` and `links` keys
213
+
214
+ ## Error Handling
215
+
216
+ The gem provides detailed error responses for various scenarios:
217
+
218
+ ### Invalid Page Size
219
+ ```json
220
+ {
221
+ "title": "Invalid Parameter.",
222
+ "detail": "page[size] is required and must be a positive integer; got 0",
223
+ "source": { "parameter": "page[size]" }
224
+ }
225
+ ```
226
+
227
+ ### Unsupported Sort
228
+ ```json
229
+ {
230
+ "title": "Unsupported Sort.",
231
+ "detail": "page[sort] is not supported; got page[sort]=name",
232
+ "source": { "parameter": "page[sort]" },
233
+ "links": { "type": ["https://jsonapi.org/profiles/ethanresnick/cursor-pagination/unsupported-sort"] }
234
+ }
235
+ ```
236
+
237
+ ### Range Pagination Not Supported
238
+ ```json
239
+ {
240
+ "title": "Range Pagination Not Supported.",
241
+ "detail": "Range pagination not supported; got page[before]=123 and page[after]=456",
242
+ "links": { "type": ["https://jsonapi.org/profiles/ethanresnick/cursor-pagination/range-pagination-not-supported"] }
243
+ }
244
+ ```
245
+
246
+ ## Development
247
+
248
+ 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.
249
+
250
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
251
+
252
+ ### Running Tests
253
+
254
+ ```bash
255
+ # Run all tests
256
+ bundle exec rake spec
257
+
258
+ # Run with coverage
259
+ bundle exec rspec
260
+
261
+ # Run linting
262
+ bundle exec rubocop
263
+ ```
264
+
265
+ ### Code Quality
266
+
267
+ This gem follows Ruby best practices and includes:
268
+
269
+ - RuboCop for code style enforcement
270
+ - RSpec for comprehensive testing
271
+ - Semantic versioning
272
+ - Changelog maintenance
273
+
274
+ ## Performance Considerations
275
+
276
+ Cursor-based pagination is generally more efficient than offset-based pagination, especially for large datasets. However, keep these points in mind:
277
+
278
+ 1. **Database Indexes**: Ensure your cursor field is properly indexed
279
+ 2. **Query Complexity**: Complex joins may impact performance
280
+ 3. **Total Count**: The `total_size` calculation runs a separate COUNT query
281
+ 4. **Memory Usage**: Large page sizes will use more memory
282
+
283
+ ## Comparison with Offset Pagination
284
+
285
+ | Feature | Cursor Pagination | Offset Pagination |
286
+ |---------|------------------|-------------------|
287
+ | Performance on large datasets | ✅ Excellent | ❌ Degrades |
288
+ | Consistent results during data changes | ✅ Yes | ❌ No |
289
+ | Jump to arbitrary page | ❌ No | ✅ Yes |
290
+ | Bi-directional navigation | ✅ Yes | ✅ Yes |
291
+ | Implementation complexity | 🟡 Medium | ✅ Simple |
292
+
293
+ ## Contributing
294
+
295
+ Bug reports and pull requests are welcome on GitHub at https://github.com/prashm/api_cursor_pagination. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/prashm/api_cursor_pagination/blob/main/CODE_OF_CONDUCT.md).
296
+
297
+ ## License
298
+
299
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
300
+
301
+ ## Code of Conduct
302
+
303
+ Everyone interacting in the ApiCursorPagination project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/prashm/api_cursor_pagination/blob/main/CODE_OF_CONDUCT.md).
304
+
305
+ ## Changelog
306
+
307
+ See [CHANGELOG.md](CHANGELOG.md) for a list of changes.
308
+
309
+ ## Support
310
+
311
+ If you have any questions or issues, please:
312
+
313
+ 1. Check the [documentation](https://github.com/prashm/api_cursor_pagination)
314
+ 2. Search [existing issues](https://github.com/prashm/api_cursor_pagination/issues)
315
+ 3. Create a [new issue](https://github.com/prashm/api_cursor_pagination/issues/new) if needed
316
+
317
+ ## Acknowledgments
318
+
319
+ - Based on the [JSON:API cursor pagination profile](https://jsonapi.org/profiles/ethanresnick/cursor-pagination/)
320
+ - Inspired by best practices from various pagination implementations
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "rubocop/rake_task"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
9
+
10
+ desc "Run tests"
11
+ task default: %i[spec rubocop]
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/api_cursor_pagination/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "api_cursor_pagination"
7
+ spec.version = ApiCursorPagination::VERSION
8
+ spec.authors = ["Prashant Mokkarala"]
9
+ spec.email = ["prashm@gmail.com"]
10
+
11
+ spec.summary = "A Rails concern for implementing cursor-based pagination in APIs"
12
+ spec.description = "ApiCursorPagination provides a Rails concern that implements cursor-based pagination following the JSON:API cursor pagination profile. It allows for efficient pagination of large datasets by using cursor-based navigation instead of offset-based pagination."
13
+ spec.homepage = "https://github.com/prashm/api_cursor_pagination"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.5.0"
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "https://github.com/prashm/api_cursor_pagination"
20
+ spec.metadata["changelog_uri"] = "https://github.com/prashm/api_cursor_pagination/blob/main/CHANGELOG.md"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(__dir__) do
25
+ `git ls-files -z`.split("\x0").reject do |f|
26
+ (File.expand_path(f) == __FILE__) ||
27
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
28
+ end
29
+ end
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ # Runtime dependencies
35
+ spec.add_dependency "activesupport", ">= 5.0"
36
+ spec.add_dependency "railties", ">= 5.0"
37
+
38
+ # Development dependencies
39
+ spec.add_development_dependency "bundler", "~> 2.0"
40
+ spec.add_development_dependency "rake", "~> 13.0"
41
+ spec.add_development_dependency "rspec", "~> 3.0"
42
+ spec.add_development_dependency "rails", ">= 5.0"
43
+ spec.add_development_dependency "sqlite3", "~> 1.4"
44
+ spec.add_development_dependency "rubocop", "~> 1.21"
45
+ spec.add_development_dependency "rubocop-rails", "~> 2.0"
46
+ spec.add_development_dependency "rubocop-rspec", "~> 2.0"
47
+
48
+ # For more information and examples about making a new gem, check out our
49
+ # guide at: https://bundler.io/guides/creating_gem.html
50
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ # Include this controller concern in your api controller class to enable cursor pagination
6
+ # Example:
7
+ # class ExampleApiController
8
+ # include ApiCursorPagination::Concern
9
+ #
10
+ # def index
11
+ # validate_and_setup_page_params(params)
12
+ #
13
+ # if @errors.blank?
14
+ # query_scope = build_your_sql_query_scope(params)
15
+ # rows = paginate(query_scope, 'row_id')
16
+ #
17
+ # api_response = { status: 'Success', field_names: field_names }.
18
+ # merge(page_links_and_meta_data(request.original_url.split('?').first, request.query_parameters)).
19
+ # merge(results: rows).to_json
20
+ # render(json: api_response, status: 201)
21
+ # else
22
+ # render(json: status400_error_response.to_json, status: 400)
23
+ # end
24
+ # end
25
+ #
26
+ # private
27
+ #
28
+ # def status400_error_response
29
+ # response = { status: 'Error', errors: [] }
30
+ # @errors.each do |error|
31
+ # response[:errors] << (error.is_a?(Hash) ? error : { title: error })
32
+ # end
33
+ # response
34
+ # end
35
+ # end
36
+
37
+ module ApiCursorPagination
38
+ module Concern
39
+ extend ActiveSupport::Concern
40
+
41
+ # Based on https://jsonapi.org/profiles/ethanresnick/cursor-pagination/
42
+
43
+ attr_accessor :page_size, :page_before, :page_after,
44
+ # Meta attrs
45
+ :total_size, :total_pages, :next_page_cursor_id, :prev_page_cursor_id
46
+
47
+ def validate_and_setup_page_params(page_params)
48
+ @errors ||= []
49
+ self.page_size = 0
50
+ return if page_params.blank?
51
+
52
+ self.page_size = page_params[:size].to_i
53
+ if page_size < 1
54
+ @errors << {
55
+ title: 'Invalid Parameter.',
56
+ detail: "page[size] is required and must be a positive integer; got #{page_params[:size]}",
57
+ source: { parameter: "page[size]" }
58
+ }
59
+ elsif page_params.has_key?(:sort)
60
+ @errors << {
61
+ title: 'Unsupported Sort.',
62
+ detail: "page[sort] is not supported; got page[sort]=#{page_params[:sort]}",
63
+ source: { parameter: "page[sort]" },
64
+ links: { type: ["https://jsonapi.org/profiles/ethanresnick/cursor-pagination/unsupported-sort"] }
65
+ }
66
+ elsif page_params.has_key?(:before) && page_params.has_key?(:after)
67
+ @errors << {
68
+ title: "Range Pagination Not Supported.",
69
+ detail: "Range pagination not supported; got page[before]=#{page_params[:before]} and page[after]=#{page_params[:after]}",
70
+ links: { type: ["https://jsonapi.org/profiles/ethanresnick/cursor-pagination/range-pagination-not-supported"] }
71
+ }
72
+ elsif page_params.has_key?(:before)
73
+ self.page_before = page_params[:before]
74
+ if self.page_before.blank?
75
+ @errors << {
76
+ title: 'Invalid Parameter.',
77
+ detail: 'page[before] is invalid',
78
+ source: { parameter: 'page[before]' }
79
+ }
80
+ end
81
+ elsif page_params.has_key?(:after)
82
+ self.page_after = page_params[:after]
83
+ if self.page_after.blank?
84
+ @errors << {
85
+ title: 'Invalid Parameter.',
86
+ detail: 'page[after] is invalid',
87
+ source: { parameter: 'page[after]' }
88
+ }
89
+ end
90
+ end
91
+
92
+ end
93
+
94
+ def paginate(scope, scope_id_str, row_id_str = scope_id_str)
95
+ if page_size > 0
96
+ self.total_size = scope.size
97
+ self.total_pages = total_size / page_size
98
+ self.total_pages += 1 if total_size % page_size > 0
99
+
100
+ scope = scope.having("#{scope_id_str} > ?", page_after) if page_after.present?
101
+ scope = scope.having("#{scope_id_str} < ?", page_before) if page_before.present?
102
+ scope = scope.limit(page_size)
103
+ if page_before.present? && page_after.blank?
104
+ scope = scope.order("#{scope_id_str} desc")
105
+ rows = scope.to_a.sort_by{|r| r.send(row_id_str)}
106
+ else
107
+ rows = scope.order(scope_id_str).to_a
108
+ end
109
+
110
+ self.next_page_cursor_id = rows.last&.send(row_id_str)
111
+ self.prev_page_cursor_id = rows.first&.send(row_id_str)
112
+ rows
113
+ else
114
+ scope.to_a
115
+ end
116
+ end
117
+
118
+ def page_links_and_meta_data(base_url, query_params)
119
+ if page_size > 0
120
+ cursor = {}
121
+ cursor[:before] = prev_page_cursor_id if prev_page_cursor_id.present?
122
+ cursor[:after] = next_page_cursor_id if next_page_cursor_id.present?
123
+ meta_hash = {
124
+ meta: {
125
+ page: { cursor: cursor, total: total_size, pages: total_pages }
126
+ }
127
+ }
128
+ meta_hash[:links] = {}
129
+ if prev_page_cursor_id.present?
130
+ query_string = query_params.merge(page: { before: prev_page_cursor_id, size: page_size }).to_query
131
+ meta_hash[:links].merge!(prev: "#{base_url}?#{query_string}")
132
+ end
133
+ if next_page_cursor_id.present?
134
+ query_string = query_params.merge(page: { after: next_page_cursor_id, size: page_size }).to_query
135
+ meta_hash[:links].merge!(next: "#{base_url}?#{query_string}")
136
+ end
137
+ meta_hash
138
+ else
139
+ {}
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiCursorPagination
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "api_cursor_pagination/version"
4
+ require_relative "api_cursor_pagination/concern"
5
+
6
+ module ApiCursorPagination
7
+ class Error < StandardError; end
8
+ end
metadata ADDED
@@ -0,0 +1,196 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: api_cursor_pagination
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Prashant Mokkarala
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '5.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '5.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: railties
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '5.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: bundler
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '13.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '13.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rspec
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rails
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '5.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '5.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: sqlite3
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '1.4'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '1.4'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rubocop
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '1.21'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '1.21'
124
+ - !ruby/object:Gem::Dependency
125
+ name: rubocop-rails
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '2.0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '2.0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: rubocop-rspec
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '2.0'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '2.0'
152
+ description: ApiCursorPagination provides a Rails concern that implements cursor-based
153
+ pagination following the JSON:API cursor pagination profile. It allows for efficient
154
+ pagination of large datasets by using cursor-based navigation instead of offset-based
155
+ pagination.
156
+ email:
157
+ - prashm@gmail.com
158
+ executables: []
159
+ extensions: []
160
+ extra_rdoc_files: []
161
+ files:
162
+ - CHANGELOG.md
163
+ - LICENSE
164
+ - LICENSE.txt
165
+ - README.md
166
+ - Rakefile
167
+ - api_cursor_pagination.gemspec
168
+ - lib/api_cursor_pagination.rb
169
+ - lib/api_cursor_pagination/concern.rb
170
+ - lib/api_cursor_pagination/version.rb
171
+ homepage: https://github.com/prashm/api_cursor_pagination
172
+ licenses:
173
+ - MIT
174
+ metadata:
175
+ allowed_push_host: https://rubygems.org
176
+ homepage_uri: https://github.com/prashm/api_cursor_pagination
177
+ source_code_uri: https://github.com/prashm/api_cursor_pagination
178
+ changelog_uri: https://github.com/prashm/api_cursor_pagination/blob/main/CHANGELOG.md
179
+ rdoc_options: []
180
+ require_paths:
181
+ - lib
182
+ required_ruby_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: 2.5.0
187
+ required_rubygems_version: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - ">="
190
+ - !ruby/object:Gem::Version
191
+ version: '0'
192
+ requirements: []
193
+ rubygems_version: 3.6.7
194
+ specification_version: 4
195
+ summary: A Rails concern for implementing cursor-based pagination in APIs
196
+ test_files: []