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 +7 -0
- data/CHANGELOG.md +34 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +320 -0
- data/Rakefile +11 -0
- data/api_cursor_pagination.gemspec +50 -0
- data/lib/api_cursor_pagination/concern.rb +143 -0
- data/lib/api_cursor_pagination/version.rb +5 -0
- data/lib/api_cursor_pagination.rb +8 -0
- metadata +196 -0
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
|
+
[](https://badge.fury.io/rb/api_cursor_pagination)
|
4
|
+
[](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,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
|
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: []
|