strict_pagination 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: 7234dbd288be581e5a708b4c854d4918e7ddb7f79c80c13d4be3f3cfd681bf23
4
- data.tar.gz: 9edfc21298022007fa6727da2d93b92ee5ff160ab9bb094de7a43f24749c14f6
3
+ metadata.gz: 88249f37e3e76bb824c4c0a527bf42d5206562f239c55896d30459990b9047f9
4
+ data.tar.gz: c0ecab0ea113dbbfdd792a7854f21011af99e703e8038281a04e1edd42951aad
5
5
  SHA512:
6
- metadata.gz: c4fea6505c3b97324c0520271ddfbd722aacec173ab014fd969f459de5d6fca545b7eb5fb5cf59a41e17344a7948ebaf582c17925f4c52f8b61d1b027f7a8126
7
- data.tar.gz: e074c0ebcbf7cc4664ecfad44552b1ba67f58478b6602b2af14a82d67af45d2f3d4781cdbf09a5cd200d66946fe368c66d3ba0ba93908d298f1a6124a3d5b20b
6
+ metadata.gz: 40be886cb36389573902cbdaf8cfdda2ec8161b94dbe0df1ab0c35ac2219c709d368c8178a1597205e5ab1fe2d7954fa99bbaa8031c3f7d86289a8494d610ab1
7
+ data.tar.gz: 15db3c489fa654ad9b2cde3529b03986a74a5a0d8a8fd5111eec78d6ff22043b0c734023b4b8f02463d385d7140f70bb9a4242fb2cceefbaccc0dc2f9bcd88bb
data/README.md CHANGED
@@ -1,7 +1,12 @@
1
1
  # StrictPagination
2
2
 
3
+ > **Keywords:** `pagination`, `rails`, `ruby`, `activerecord`, `api`, `validation`, `params`, `database`, `sql`, `query-validation`
4
+
3
5
  A Ruby gem that adds strict pagination validation to ActiveRecord, preventing unsafe JOINs that can cause inconsistent pagination results.
4
6
 
7
+ [![Gem Version](https://badge.fury.io/rb/strict_pagination.svg)](https://badge.fury.io/rb/strict_pagination)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+
5
10
  ## Problem
6
11
 
7
12
  When using LIMIT/OFFSET with DISTINCT and JOINs to associations that multiply rows (like `has_many` or unsafe `has_one`), pagination can become inconsistent. This happens because:
@@ -15,6 +20,11 @@ When using LIMIT/OFFSET with DISTINCT and JOINs to associations that multiply ro
15
20
 
16
21
  This gem provides `strict_pagination` mode that validates queries before execution, ensuring they don't use JOINs that multiply rows when using pagination.
17
22
 
23
+ ## Requirements
24
+
25
+ - Ruby 3.1.0 or higher
26
+ - ActiveRecord 6.1 or higher (Rails 6.1+)
27
+
18
28
  ## Installation
19
29
 
20
30
  Add this line to your application's Gemfile:
@@ -43,29 +53,29 @@ Simply add `.strict_pagination` to your ActiveRecord queries with DISTINCT:
43
53
 
44
54
  ```ruby
45
55
  # Safe: no associations
46
- User.strict_pagination.distinct.page(1).per(10)
56
+ User.strict_pagination.distinct.limit(10).offset(0)
47
57
 
48
58
  # Safe: belongs_to associations don't multiply rows
49
59
  Article.strict_pagination
50
60
  .includes(:author)
51
61
  .distinct
52
- .page(1).per(10)
62
+ .limit(10).offset(0)
53
63
 
54
64
  # Safe: view-backed has_one associations
55
65
  Order.strict_pagination
56
66
  .includes(:latest_payment) # backed by a database view
57
67
  .distinct
58
- .page(1).per(10)
68
+ .limit(10).offset(0)
59
69
 
60
70
  # ERROR: has_many multiplies rows
61
71
  User.strict_pagination
62
72
  .includes(:posts)
63
73
  .distinct
64
- .page(1).per(10)
74
+ .limit(10).offset(0)
65
75
  # => StrictPagination::ViolationError
66
76
  ```
67
77
 
68
- **Note:** Validation only runs when **both** `LIMIT` (from `.page()`) and `DISTINCT` are present. This is because the pagination issue only occurs with DISTINCT queries.
78
+ **Note:** Validation only runs when **both** `LIMIT` (from `.limit()`) and `DISTINCT` are present. This is because the pagination issue only occurs with DISTINCT queries.
69
79
 
70
80
  ### When is Validation Triggered?
71
81
 
@@ -155,7 +165,7 @@ end
155
165
 
156
166
  # Now these are considered safe
157
167
  class Reports::UserSummary < ApplicationRecord; end
158
- Order.strict_pagination.includes(:user_summary).page(1) # Safe
168
+ Order.strict_pagination.includes(:user_summary).limit(20) # Safe
159
169
  ```
160
170
 
161
171
  ## Best Practices
@@ -167,11 +177,14 @@ Use `strict_pagination` on all paginated API endpoints with DISTINCT:
167
177
  ```ruby
168
178
  # app/controllers/api/v1/users_controller.rb
169
179
  def index
180
+ page = (params[:page] || 1).to_i
181
+ per_page = (params[:per_page] || 20).to_i
182
+
170
183
  @users = User.strict_pagination
171
184
  .includes(:profile) # Safe: belongs_to
172
185
  .distinct
173
- .page(params[:page])
174
- .per(params[:per_page])
186
+ .limit(per_page)
187
+ .offset((page - 1) * per_page)
175
188
 
176
189
  render json: @users
177
190
  end
@@ -201,7 +214,7 @@ end
201
214
  Author.strict_pagination
202
215
  .includes(:latest_book)
203
216
  .distinct
204
- .page(1).per(10)
217
+ .limit(10).offset(0)
205
218
  ```
206
219
 
207
220
  ### Gradual Adoption
@@ -212,15 +225,27 @@ You can gradually adopt strict pagination in your codebase:
212
225
  # Start with critical endpoints
213
226
  class Api::V1::OrdersController < ApplicationController
214
227
  def index
215
- @orders = Order.strict_pagination.distinct.page(params[:page])
228
+ page = (params[:page] || 1).to_i
229
+ per_page = 20
230
+
231
+ @orders = Order.strict_pagination
232
+ .distinct
233
+ .limit(per_page)
234
+ .offset((page - 1) * per_page)
216
235
  end
217
236
  end
218
237
 
219
238
  # Once confident, use it everywhere
220
239
  class ApplicationRecord < ActiveRecord::Base
221
- # Override default scope to use strict_pagination
222
- def self.paginated(**options)
223
- strict_pagination.distinct.page(options[:page]).per(options[:per_page])
240
+ # Helper method for pagination
241
+ def self.paginated(page: 1, per_page: 20)
242
+ page = page.to_i
243
+ per_page = per_page.to_i
244
+
245
+ strict_pagination
246
+ .distinct
247
+ .limit(per_page)
248
+ .offset((page - 1) * per_page)
224
249
  end
225
250
  end
226
251
  ```
@@ -250,25 +275,25 @@ The gem follows Rails best practices:
250
275
 
251
276
  ```ruby
252
277
  # No associations
253
- User.strict_pagination.distinct.page(1).per(20)
278
+ User.strict_pagination.distinct.limit(20).offset(0)
254
279
 
255
280
  # belongs_to only
256
281
  Comment.strict_pagination
257
282
  .includes(:author, :post)
258
283
  .distinct
259
- .page(1).per(20)
284
+ .limit(20).offset(0)
260
285
 
261
286
  # View-backed has_one
262
287
  Invoice.strict_pagination
263
288
  .includes(:latest_payment)
264
289
  .distinct
265
- .page(1).per(20)
290
+ .limit(20).offset(0)
266
291
 
267
292
  # has_one with unique constraint
268
293
  User.strict_pagination
269
294
  .includes(:profile) # profiles.user_id has unique index
270
295
  .distinct
271
- .page(1).per(20)
296
+ .limit(20).offset(0)
272
297
  ```
273
298
 
274
299
  ### Unsafe Queries
@@ -278,19 +303,19 @@ User.strict_pagination
278
303
  User.strict_pagination
279
304
  .includes(:posts) # Error: posts multiplies rows
280
305
  .distinct
281
- .page(1).per(20)
306
+ .limit(20).offset(0)
282
307
 
283
308
  # has_and_belongs_to_many
284
309
  Article.strict_pagination
285
310
  .includes(:tags) # Error: tags multiplies rows
286
311
  .distinct
287
- .page(1).per(20)
312
+ .limit(20).offset(0)
288
313
 
289
314
  # has_one without unique constraint
290
315
  Account.strict_pagination
291
316
  .includes(:primary_contact) # Error: no unique constraint
292
317
  .distinct
293
- .page(1).per(20)
318
+ .limit(20).offset(0)
294
319
  ```
295
320
 
296
321
  ## Development
@@ -317,6 +342,28 @@ gem build strict_pagination.gemspec
317
342
 
318
343
  Bug reports and pull requests are welcome on GitHub at https://github.com/hdamico/strict_pagination.
319
344
 
345
+ Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
346
+
347
+ ## Roadmap
348
+
349
+ - [ ] Add helper methods for easier pagination parameter handling
350
+ - [ ] Performance benchmarks and optimization guide
351
+ - [ ] Rails generator for adding strict_pagination to existing controllers
352
+ - [ ] Automated migration helper for converting has_many to view-backed has_one
353
+ - [ ] Support for custom validation rules
354
+
355
+ ## Community & Support
356
+
357
+ - **Issues:** Report bugs or request features at [GitHub Issues](https://github.com/hdamico/strict_pagination/issues)
358
+ - **Discussions:** Ask questions and share ideas at [GitHub Discussions](https://github.com/hdamico/strict_pagination/discussions)
359
+ - **Contributing:** See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines
360
+
320
361
  ## License
321
362
 
322
363
  The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
364
+
365
+ ---
366
+
367
+ **Made with ❤️ for the Ruby and Rails community**
368
+
369
+ *If you find this gem useful, please ⭐ star the repository and share it with others!*
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "strict_pagination/version"
4
- require "strict_pagination/validator"
5
- require "strict_pagination/relation_methods"
6
- require "strict_pagination/relation_extension"
7
- require "active_support/lazy_load_hooks"
3
+ require 'strict_pagination/version'
4
+ require 'strict_pagination/validator'
5
+ require 'strict_pagination/relation_methods'
6
+ require 'strict_pagination/relation_extension'
7
+ require 'active_support/lazy_load_hooks'
8
8
 
9
9
  ActiveSupport.on_load :active_record do
10
- require "active_record"
10
+ require 'active_record'
11
11
 
12
- ::ActiveRecord::Relation.include StrictPagination::RelationMethods
13
- ::ActiveRecord::Relation.include StrictPagination::Validator
14
- ::ActiveRecord::Relation.prepend StrictPagination::RelationExtension
12
+ ActiveRecord::Relation.include StrictPagination::RelationMethods
13
+ ActiveRecord::Relation.include StrictPagination::Validator
14
+ ActiveRecord::Relation.prepend StrictPagination::RelationExtension
15
15
  end
@@ -2,9 +2,9 @@
2
2
 
3
3
  module StrictPagination
4
4
  class Railtie < ::Rails::Railtie
5
- initializer "strict_pagination.configure_rails_initialization" do
5
+ initializer 'strict_pagination.configure_rails_initialization' do
6
6
  ActiveSupport.on_load(:active_record) do
7
- require "strict_pagination/active_record"
7
+ require 'strict_pagination/active_record'
8
8
  end
9
9
  end
10
10
  end
@@ -65,9 +65,7 @@ module StrictPagination
65
65
  return unless limit_value
66
66
 
67
67
  # Check if we should validate without DISTINCT requirement
68
- unless StrictPagination.config.validate_on_all_queries
69
- return unless distinct_value
70
- end
68
+ return if !StrictPagination.config.validate_on_all_queries && !distinct_value
71
69
 
72
70
  # Get all unsafe associations that are actually included/joined
73
71
  unsafe_associations = detect_unsafe_included_associations
@@ -47,8 +47,8 @@ module StrictPagination
47
47
  klass_obj = reflection.klass
48
48
 
49
49
  # Check if it's a database view (default prefixes)
50
- return true if klass_obj.name.start_with?("Views::")
51
- return true if klass_obj.table_name.start_with?("views_")
50
+ return true if klass_obj.name.start_with?('Views::')
51
+ return true if klass_obj.table_name.start_with?('views_')
52
52
 
53
53
  # Check custom view prefixes from config
54
54
  StrictPagination.config.safe_view_prefixes.each do |prefix|
@@ -119,7 +119,7 @@ module StrictPagination
119
119
  end
120
120
 
121
121
  def build_error_message(unsafe_associations)
122
- associations_list = unsafe_associations.map { |name| "`#{name}`" }.join(", ")
122
+ associations_list = unsafe_associations.map { |name| "`#{name}`" }.join(', ')
123
123
  associations_details = unsafe_associations.map { |a| " - #{a}: #{describe_association_type(a)}" }.join("\n")
124
124
 
125
125
  <<~MSG.squish
@@ -141,16 +141,16 @@ module StrictPagination
141
141
  # Describes the association type for error messages.
142
142
  def describe_association_type(association_name)
143
143
  reflection = klass.reflect_on_association(association_name.to_sym)
144
- return "unknown" unless reflection
144
+ return 'unknown' unless reflection
145
145
 
146
146
  association_type_description(reflection.macro)
147
147
  end
148
148
 
149
149
  def association_type_description(macro)
150
150
  case macro
151
- when :has_many then "has_many (always multiplies rows)"
152
- when :has_and_belongs_to_many then "has_and_belongs_to_many (always multiplies rows)"
153
- when :has_one then "has_one without unique constraint (can multiply rows)"
151
+ when :has_many then 'has_many (always multiplies rows)'
152
+ when :has_and_belongs_to_many then 'has_and_belongs_to_many (always multiplies rows)'
153
+ when :has_one then 'has_one without unique constraint (can multiply rows)'
154
154
  else macro.to_s
155
155
  end
156
156
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StrictPagination
4
- VERSION = "0.1.1"
4
+ VERSION = '0.2.0'
5
5
  end
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/lazy_load_hooks"
4
- require "strict_pagination/version"
5
- require "strict_pagination/config"
3
+ require 'active_record'
4
+ require 'active_support/lazy_load_hooks'
5
+ require 'strict_pagination/version'
6
+ require 'strict_pagination/config'
6
7
 
7
8
  module StrictPagination
8
9
  # Custom error raised when attempting pagination with unsafe JOINs
@@ -12,8 +13,8 @@ end
12
13
 
13
14
  # Load Rails integration if Rails is available
14
15
  if defined?(Rails::Railtie)
15
- require "strict_pagination/railtie"
16
+ require 'strict_pagination/railtie'
16
17
  else
17
18
  # For non-Rails environments, load ActiveRecord integration directly
18
- require "strict_pagination/active_record"
19
+ require 'strict_pagination/active_record'
19
20
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strict_pagination
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
  - Hernán d'Amico
@@ -15,7 +15,7 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '6.0'
18
+ version: '6.1'
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
21
  version: '8.0'
@@ -25,41 +25,14 @@ dependencies:
25
25
  requirements:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
- version: '6.0'
28
+ version: '6.1'
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: '8.0'
32
- - !ruby/object:Gem::Dependency
33
- name: rake
34
- requirement: !ruby/object:Gem::Requirement
35
- requirements:
36
- - - "~>"
37
- - !ruby/object:Gem::Version
38
- version: '13.0'
39
- type: :development
40
- prerelease: false
41
- version_requirements: !ruby/object:Gem::Requirement
42
- requirements:
43
- - - "~>"
44
- - !ruby/object:Gem::Version
45
- version: '13.0'
46
- - !ruby/object:Gem::Dependency
47
- name: rspec
48
- requirement: !ruby/object:Gem::Requirement
49
- requirements:
50
- - - "~>"
51
- - !ruby/object:Gem::Version
52
- version: '3.0'
53
- type: :development
54
- prerelease: false
55
- version_requirements: !ruby/object:Gem::Requirement
56
- requirements:
57
- - - "~>"
58
- - !ruby/object:Gem::Version
59
- version: '3.0'
60
- description: Adds strict_pagination mode to ActiveRecord::Relation to validate queries
61
- before execution, ensuring they don't use JOINs that multiply rows (has_many, unsafe
62
- has_one). Helps prevent inconsistent pagination with LIMIT/OFFSET + DISTINCT.
32
+ description: StrictPagination enforces type-safe pagination parameters and validates
33
+ queries before execution, preventing unsafe JOINs (has_many, unsafe has_one) that
34
+ multiply rows and cause inconsistent pagination with LIMIT/OFFSET + DISTINCT. Provides
35
+ helpers for ActiveRecord and API responses.
63
36
  email:
64
37
  - hernan.damico67@gmail.com
65
38
  executables: []
@@ -80,8 +53,10 @@ homepage: https://github.com/hdamico/strict_pagination
80
53
  licenses:
81
54
  - MIT
82
55
  metadata:
83
- source_code_uri: https://github.com/hdamico/strict_pagination
56
+ homepage_uri: https://github.com/hdamico/strict_pagination
84
57
  changelog_uri: https://github.com/hdamico/strict_pagination/blob/main/CHANGELOG.md
58
+ bug_tracker_uri: https://github.com/hdamico/strict_pagination/issues
59
+ documentation_uri: https://github.com/hdamico/strict_pagination/blob/main/README.md
85
60
  rdoc_options: []
86
61
  require_paths:
87
62
  - lib
@@ -89,7 +64,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
89
64
  requirements:
90
65
  - - ">="
91
66
  - !ruby/object:Gem::Version
92
- version: 2.7.0
67
+ version: 3.1.0
93
68
  required_rubygems_version: !ruby/object:Gem::Requirement
94
69
  requirements:
95
70
  - - ">="
@@ -98,5 +73,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
73
  requirements: []
99
74
  rubygems_version: 3.7.1
100
75
  specification_version: 4
101
- summary: Strict pagination validation for ActiveRecord to prevent unsafe JOINs
76
+ summary: Strict and safe pagination for Ruby/Rails apps with ActiveRecord
102
77
  test_files: []