jsonapi-query_builder 0.2.1 → 0.4.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: f47ebd86badc968ef8a79333f49ac496c067ad6a26c6563cca0257c1e8cdeba2
4
- data.tar.gz: ff874753c9a7ba077bf7876a8a598dd01058a0e2f7a2a982dde2fc579342a5d0
3
+ metadata.gz: e9ea254260274b96b7d2374e9b1b6940467f86d619b2507abcffc9a592fbd1e6
4
+ data.tar.gz: 21d0c563ed3593a7ff198b645e92fae84a5fbe144309a71d14c0799c09e21bf6
5
5
  SHA512:
6
- metadata.gz: fc7168dad1ea652617381662a8e75cadbb132addb9b78d6af052b15b590d3f031fa143a6508e6f2feb52f82ee9f333892551378fa5a969ac5232bb48ca8b0c8e
7
- data.tar.gz: 6ead187ee07d7d9db557c2010b3d2c992c01d05c888ad6a777716380fa565fc9aff54fb39db54120131bbb544418f789f53cade618c55db86d2aec3d03c08a61
6
+ metadata.gz: ec7b08f99bd292f0c33105e22be664f92832efee849785cc4f5da9dab1d9575993b546c10722eafd9d2e8472e4ae6d6bc23d9cff3cb48c9bdf4faa0815a9bbc8
7
+ data.tar.gz: 30f6711fab7a8be90960417e545dd2bbf7b3778a4997fceab15f56587fad040018f0a9d3f6f4f845c890e7a8ea2985771823160ff18d9e2672f93dbc59cfedf6
@@ -8,7 +8,7 @@ on:
8
8
 
9
9
  jobs:
10
10
  standardrb:
11
- runs-on: ubuntu-18.04
11
+ runs-on: ubuntu-24.04
12
12
 
13
13
  steps:
14
14
  - uses: actions/checkout@v2
@@ -19,7 +19,7 @@ jobs:
19
19
  run: bundle exec standardrb
20
20
 
21
21
  rubocop-rspec:
22
- runs-on: ubuntu-18.04
22
+ runs-on: ubuntu-24.04
23
23
 
24
24
  steps:
25
25
  - uses: actions/checkout@v2
@@ -8,10 +8,10 @@ on:
8
8
 
9
9
  jobs:
10
10
  rake-spec:
11
- runs-on: ubuntu-18.04
11
+ runs-on: ubuntu-24.04
12
12
  strategy:
13
13
  matrix:
14
- ruby: [2.5, 2.6, 2.7, 3.0]
14
+ ruby: ['3.2', '3.3', '3.4']
15
15
 
16
16
  steps:
17
17
  - uses: actions/checkout@v2
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.0.2
1
+ 3.4.5
data/CHANGELOG.md CHANGED
@@ -1,20 +1,46 @@
1
1
  # Change log
2
2
 
3
+ ## 0.4.0 (2025-11-03)
4
+
5
+ ### Enhancements
6
+ - Added support for dynamic (prefix-based) sorting [#31](https://github.com/infinum/jsonapi-query_builder/pull/31)
7
+ - Added Pagy countless paginator [#26](https://github.com/infinum/jsonapi-query_builder/pull/26)
8
+
9
+ ### Changes
10
+ - Dropped support for Ruby < 3.2
11
+ - Dropped Rails < 7.2 support
12
+
13
+ ## 0.3.0 (2021-12-07)
14
+
15
+ ### Enhancements
16
+
17
+ - Add support for proc and object default
18
+ sorts [a167f90](https://github.com/infinum/jsonapi-query_builder/commit/a167f90ca718fe62c0899520cd4c5c859f89035b)
19
+
3
20
  ## 0.2.1 (2021-10-04)
4
21
 
22
+ ### Bugfixes
23
+
5
24
  - [#22](https://github.com/infinum/jsonapi-query_builder/pull/22): Bump allowed pagy version.
6
25
 
7
26
  ## 0.2.0 (2021-09-29)
27
+
28
+ ### Enhancements
29
+
8
30
  Added support for Kaminari and Keyset pagination strategies in addition to Pagy.
9
31
 
10
32
  - [#21](https://github.com/infinum/jsonapi-query_builder/pull/21): Extract paginators.
11
33
 
12
34
  ## 0.1.9 (2021-05-07)
13
35
 
36
+ ### Enhancements
37
+
14
38
  - [#18](https://github.com/infinum/jsonapi-query_builder/pull/18): Remove Ruby `to` version.
15
39
  - [#9](https://github.com/infinum/jsonapi-query_builder/pull/9): added github actions
16
40
 
17
41
  ## 0.1.8 (2021-01-25)
18
42
 
43
+ ### Enhancements
44
+
19
45
  - [#8](https://github.com/infinum/jsonapi-query_builder/pull/8): add support for ruby 3.0
20
46
 
data/Gemfile.lock CHANGED
@@ -1,42 +1,56 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jsonapi-query_builder (0.2.1)
5
- activerecord (>= 5)
4
+ jsonapi-query_builder (0.4.0)
5
+ activerecord (>= 7.2)
6
6
  pagy (>= 3.5)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- actionview (6.1.4.1)
12
- activesupport (= 6.1.4.1)
11
+ actionview (8.0.2.1)
12
+ activesupport (= 8.0.2.1)
13
13
  builder (~> 3.1)
14
- erubi (~> 1.4)
15
- rails-dom-testing (~> 2.0)
16
- rails-html-sanitizer (~> 1.1, >= 1.2.0)
17
- activemodel (6.1.4.1)
18
- activesupport (= 6.1.4.1)
19
- activerecord (6.1.4.1)
20
- activemodel (= 6.1.4.1)
21
- activesupport (= 6.1.4.1)
22
- activesupport (6.1.4.1)
23
- concurrent-ruby (~> 1.0, >= 1.0.2)
14
+ erubi (~> 1.11)
15
+ rails-dom-testing (~> 2.2)
16
+ rails-html-sanitizer (~> 1.6)
17
+ activemodel (8.0.2.1)
18
+ activesupport (= 8.0.2.1)
19
+ activerecord (8.0.2.1)
20
+ activemodel (= 8.0.2.1)
21
+ activesupport (= 8.0.2.1)
22
+ timeout (>= 0.4.0)
23
+ activesupport (8.0.2.1)
24
+ base64
25
+ benchmark (>= 0.3)
26
+ bigdecimal
27
+ concurrent-ruby (~> 1.0, >= 1.3.1)
28
+ connection_pool (>= 2.2.5)
29
+ drb
24
30
  i18n (>= 1.6, < 2)
31
+ logger (>= 1.4.2)
25
32
  minitest (>= 5.1)
26
- tzinfo (~> 2.0)
27
- zeitwerk (~> 2.3)
33
+ securerandom (>= 0.3)
34
+ tzinfo (~> 2.0, >= 2.0.5)
35
+ uri (>= 0.13.1)
28
36
  ast (2.4.2)
37
+ base64 (0.3.0)
38
+ benchmark (0.4.1)
39
+ bigdecimal (3.2.3)
29
40
  builder (3.2.4)
30
41
  bundler-audit (0.9.0.1)
31
42
  bundler (>= 1.2.0, < 3)
32
43
  thor (~> 1.0)
33
44
  coderay (1.1.3)
34
- concurrent-ruby (1.1.9)
45
+ concurrent-ruby (1.3.5)
46
+ connection_pool (2.5.4)
35
47
  crass (1.0.6)
36
48
  diff-lcs (1.4.4)
37
- erubi (1.10.0)
49
+ drb (2.2.3)
50
+ erubi (1.13.1)
38
51
  i18n (1.8.10)
39
52
  concurrent-ruby (~> 1.0)
53
+ json (2.13.2)
40
54
  kaminari (1.2.1)
41
55
  activesupport (>= 4.1.0)
42
56
  kaminari-actionview (= 1.2.1)
@@ -49,33 +63,39 @@ GEM
49
63
  activerecord
50
64
  kaminari-core (= 1.2.1)
51
65
  kaminari-core (1.2.1)
66
+ language_server-protocol (3.17.0.5)
52
67
  lefthook (0.7.6)
53
- loofah (2.12.0)
68
+ lint_roller (1.1.0)
69
+ logger (1.7.0)
70
+ loofah (2.24.1)
54
71
  crass (~> 1.0.2)
55
- nokogiri (>= 1.5.9)
72
+ nokogiri (>= 1.12.0)
56
73
  method_source (1.0.0)
57
- mini_portile2 (2.6.1)
74
+ mini_portile2 (2.8.9)
58
75
  minitest (5.14.4)
59
- nokogiri (1.12.5)
60
- mini_portile2 (~> 2.6.1)
76
+ nokogiri (1.18.9)
77
+ mini_portile2 (~> 2.8.2)
61
78
  racc (~> 1.4)
62
79
  pagy (3.11.0)
63
80
  parallel (1.21.0)
64
- parser (3.0.2.0)
81
+ parser (3.3.9.0)
65
82
  ast (~> 2.4.1)
83
+ racc
84
+ prism (1.4.0)
66
85
  pry (0.14.1)
67
86
  coderay (~> 1.1)
68
87
  method_source (~> 1.0)
69
88
  racc (1.5.2)
70
- rails-dom-testing (2.0.3)
71
- activesupport (>= 4.2.0)
89
+ rails-dom-testing (2.3.0)
90
+ activesupport (>= 5.0.0)
91
+ minitest
72
92
  nokogiri (>= 1.6)
73
- rails-html-sanitizer (1.4.2)
74
- loofah (~> 2.3)
93
+ rails-html-sanitizer (1.6.2)
94
+ loofah (~> 2.21)
95
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
75
96
  rainbow (3.0.0)
76
97
  rake (13.0.6)
77
- regexp_parser (2.1.1)
78
- rexml (3.2.5)
98
+ regexp_parser (2.11.2)
79
99
  rspec (3.10.0)
80
100
  rspec-core (~> 3.10.0)
81
101
  rspec-expectations (~> 3.10.0)
@@ -89,34 +109,52 @@ GEM
89
109
  diff-lcs (>= 1.2.0, < 2.0)
90
110
  rspec-support (~> 3.10.0)
91
111
  rspec-support (3.10.2)
92
- rubocop (1.20.0)
112
+ rubocop (1.80.2)
113
+ json (~> 2.3)
114
+ language_server-protocol (~> 3.17.0.2)
115
+ lint_roller (~> 1.1.0)
93
116
  parallel (~> 1.10)
94
- parser (>= 3.0.0.0)
117
+ parser (>= 3.3.0.2)
95
118
  rainbow (>= 2.2.2, < 4.0)
96
- regexp_parser (>= 1.8, < 3.0)
97
- rexml
98
- rubocop-ast (>= 1.9.1, < 2.0)
119
+ regexp_parser (>= 2.9.3, < 3.0)
120
+ rubocop-ast (>= 1.46.0, < 2.0)
99
121
  ruby-progressbar (~> 1.7)
100
- unicode-display_width (>= 1.4.0, < 3.0)
101
- rubocop-ast (1.12.0)
102
- parser (>= 3.0.1.1)
103
- rubocop-performance (1.11.5)
104
- rubocop (>= 1.7.0, < 2.0)
105
- rubocop-ast (>= 0.4.0)
122
+ unicode-display_width (>= 2.4.0, < 4.0)
123
+ rubocop-ast (1.46.0)
124
+ parser (>= 3.3.7.2)
125
+ prism (~> 1.4)
126
+ rubocop-performance (1.25.0)
127
+ lint_roller (~> 1.1)
128
+ rubocop (>= 1.75.0, < 2.0)
129
+ rubocop-ast (>= 1.38.0, < 2.0)
106
130
  rubocop-rspec (2.5.0)
107
131
  rubocop (~> 1.19)
108
132
  ruby-progressbar (1.11.0)
109
- sqlite3 (1.4.2)
110
- standard (1.3.0)
111
- rubocop (= 1.20.0)
112
- rubocop-performance (= 1.11.5)
133
+ securerandom (0.4.1)
134
+ sqlite3 (2.7.3)
135
+ mini_portile2 (~> 2.8.0)
136
+ standard (1.51.0)
137
+ language_server-protocol (~> 3.17.0.2)
138
+ lint_roller (~> 1.0)
139
+ rubocop (~> 1.80.2)
140
+ standard-custom (~> 1.0.0)
141
+ standard-performance (~> 1.8)
142
+ standard-custom (1.0.2)
143
+ lint_roller (~> 1.0)
144
+ rubocop (~> 1.50)
145
+ standard-performance (1.8.0)
146
+ lint_roller (~> 1.1)
147
+ rubocop-performance (~> 1.25.0)
113
148
  standardrb (1.0.0)
114
149
  standard
115
- thor (1.1.0)
116
- tzinfo (2.0.4)
150
+ thor (1.4.0)
151
+ timeout (0.4.3)
152
+ tzinfo (2.0.6)
117
153
  concurrent-ruby (~> 1.0)
118
- unicode-display_width (2.1.0)
119
- zeitwerk (2.4.2)
154
+ unicode-display_width (3.2.0)
155
+ unicode-emoji (~> 4.1)
156
+ unicode-emoji (4.1.0)
157
+ uri (1.0.4)
120
158
 
121
159
  PLATFORMS
122
160
  ruby
@@ -137,4 +175,4 @@ DEPENDENCIES
137
175
  standardrb
138
176
 
139
177
  BUNDLED WITH
140
- 2.2.22
178
+ 2.7.2
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Jsonapi::QueryBuilder ![lint](https://github.com/infinum/jsonapi-query_builder/workflows/lint/badge.svg)![spec](https://github.com/infinum/jsonapi-query_builder/workflows/spec/badge.svg)
2
2
 
3
3
  `Jsonapi::QueryBuilder` serves the purpose of adding the json api query related SQL conditions to the already scoped
4
- collection, usually used in controller index actions.
4
+ collection, usually used in controller index actions.
5
5
 
6
6
  With the query builder we can easily define logic for query filters, attributes by which we can sort, and delegate
7
7
  pagination parameters to the underlying paginator. Included relationships are automatically included via the
@@ -27,9 +27,9 @@ Or install it yourself as:
27
27
 
28
28
  ```ruby
29
29
  class UserQuery < Jsonapi::QueryBuilder::BaseQuery
30
- ## pagination
30
+ ## pagination
31
31
  paginator Jsonapi::QueryBuilder::Paginator::Pagy # default paginator
32
-
32
+
33
33
  ## sorting
34
34
  default_sort created_at: :desc
35
35
  sorts_by :last_name
@@ -54,21 +54,23 @@ end
54
54
  ```
55
55
 
56
56
  The query class is initialized using a collection and query parameters. Since query parameters are referenced explicitly
57
- we can pass them as an unsafe hash. `Jsonapi::QueryBuilder::BaseQuery` should not be responsible for scoping records based on
58
- current user permissions, or for any other type of scoping. It's only responsibility is to support the `json:api`
59
- querying. Use `pundit` or similar for policy scoping, custom query objects for other scoping, and then pass the scoped
60
- collection to the `Jsonapi::QueryBuilder::BaseQuery` object.
57
+ we can pass them as an unsafe hash. `Jsonapi::QueryBuilder::BaseQuery` should not be responsible for scoping records
58
+ based on current user permissions, or for any other type of scoping. It's only responsibility is to support
59
+ the `json:api` querying. Use `pundit` or similar for policy scoping, custom query objects for other scoping, and then
60
+ pass the scoped collection to the `Jsonapi::QueryBuilder::BaseQuery` object.
61
61
 
62
62
  ### Pagination
63
+
63
64
  Pagination support is configurable using the `paginator` method to define the paginator. It defaults to the `Pagy`
64
- paginator, a lightweight and fast paginator. Other paginators currently supported are `Kaminari` and an implementation
65
- of keyset pagination. Before using these paginators we need to explicitly require the gems in our Gemfile and the
66
- paginator file in question.
67
- Additionally one can implement it's own paginator by inheriting from `Jsonapi::QueryBuilder::Paginator::BasePaginator`.
68
- The minimum required implementation is a `#paginate` method that receives page params and returns a page of the
69
- collection. It can return the pagination details as the second item of the returned array, that can be used in the
70
- serializer for pagination metadata.
65
+ paginator, a lightweight and fast paginator. Other paginators currently supported are `Kaminari`, `PagyCountless` and
66
+ an implementation of keyset pagination. Before using these paginators we need to explicitly require the gems in our
67
+ Gemfile and the paginator file in question. Additionally one can implement it's own paginator by inheriting
68
+ from `Jsonapi::QueryBuilder::Paginator::BasePaginator`. The minimum required implementation is a `#paginate` method that
69
+ receives page params and returns a page of the collection. It can return the pagination details as the second item of
70
+ the returned array, that can be used in the serializer for pagination metadata.
71
+
71
72
  #### Using the Kaminari Paginator
73
+
72
74
  ```ruby
73
75
  require "jsonapi/query_builder/paginator/kaminari"
74
76
 
@@ -76,64 +78,128 @@ paginator Jsonapi::QueryBuilder::Paginator::Kaminari
76
78
  ```
77
79
 
78
80
  #### Using the Keyset Paginator
81
+
79
82
  ```ruby
80
83
  require "jsonapi/query_builder/paginator/keyset"
81
84
 
82
85
  paginator Jsonapi::QueryBuilder::Paginator::Keyset
83
86
  ```
84
87
 
88
+ #### Using the Pagy Countless Paginator
89
+
90
+ ```ruby
91
+ require "jsonapi/query_builder/paginator/pagy_countless"
92
+
93
+ paginator Jsonapi::QueryBuilder::Paginator::PagyCountless
94
+ ```
95
+
85
96
  ### Sorting
97
+
86
98
  #### Ensuring deterministic results
99
+
87
100
  Sorting has a fallback to an unique attribute which defaults to the `id` attribute. This ensures deterministic paginated
88
101
  collection responses. You can override the `unique_sort_attribute` in the query object.
102
+
89
103
  ```ruby
90
104
  # set the unique sort attribute
91
105
  unique_sort_attribute :email
92
106
  # use compound unique sort attributes
93
107
  unique_sort_attributes :created_at, :email
94
108
  ````
109
+
95
110
  #### Default sort options
111
+
96
112
  The `default_sort` can be set to sort by any field like `created_at` timestamp or similar. It is only used if no sort
97
113
  parameter is set, unlike the `unique_sort_attribute` which is always appended as the last sort attribute. The parameters
98
- are passed directly to the underlying active record relation, so the usual ordering options are possible.
114
+ are passed directly to the underlying active record relation, so the usual ordering options are possible. It is also
115
+ possible to define the default sort with a lambda or by passing a sort object.
116
+
99
117
  ```ruby
118
+ default_sort :created_at
119
+ # or
100
120
  default_sort created_at: :desc
121
+ # or
122
+ default_sort ->(collection) { collection.order(created_at: :desc) }
123
+ # or
124
+ default_sort SortObject
101
125
  ```
126
+
102
127
  #### Enabling simple sorting for attributes
128
+
103
129
  `sorts_by` denotes which attributes can be used for sorting. Sorting parameters are usually parsed from the
104
130
  `json:api` sort query parameter in the order they are given. So `sort=-first_name,email` would translate to
105
- `{ first_name: :desc, email: :asc }`
131
+ `{ first_name: :desc, email: :asc }`
132
+
106
133
  ```ruby
107
134
  sorts_by :first_name
108
135
  sorts_by :email
109
136
  ```
137
+
110
138
  #### Sorting with lambdas
111
- `sorts_by` also supports passing a lambda to implement a custom order or reorder function. The parameters passed to the
139
+
140
+ `sorts_by` also supports passing a lambda to implement a custom order or reorder function. The parameters passed to the
112
141
  lamdba are collection and the direction of the order, which is either `:desc` or `:asc`.
142
+
113
143
  ```ruby
114
144
  sorts_by :first_name, ->(collection, direction) { collection.order(name: direction) }
115
145
  ```
116
146
 
117
147
  #### Sorting with sort classes
118
- But since we're devout followers of the SOLID principles, we can define a sort class that responds to
119
- `#results` method, which returns the sorted collection. Under the hood the sort class is initialized with
120
- the current scope and the direction parameter.
148
+
149
+ But since we're devout followers of the SOLID principles, we can define a sort class that responds to `#results` method,
150
+ which returns the sorted collection. Under the hood the sort class is initialized with the current scope and the
151
+ direction parameter.
152
+
153
+ #### Dynamic sorting (prefix-based)
154
+
155
+ Sometimes you want to allow sorting by a dynamic subset of attributes that share a common prefix (e.g., JSON/JSONB keys, translated columns, join records). You can register a dynamic sort by attribute prefix using `dynamically_sorts_by`.
156
+
157
+ - The configured prefix is matched against each parsed sort attribute.
158
+ - The prefix is stripped and only the dynamic part is passed to your sort handler.
159
+ - You can provide either a lambda/proc or a class. The callable receives `(collection, dynamic_attribute, direction)`.
160
+
161
+ Example with a lambda (PostgreSQL JSONB text value):
162
+
163
+ ```ruby
164
+ # Allows sorting by any key in the `data` column: e.g. sort=-data.name,data.created_at
165
+ dynamically_sorts_by :'data.', ->(collection, attribute, direction) {
166
+ # attribute is the part after the prefix, e.g. "name" or "created_at"
167
+ quoted_attribute = ActiveRecord::Base.connection.quote(attribute)
168
+ collection.order(Arel.sql("(data->>#{quoted_attribute}) #{direction}"))
169
+ }
170
+ ```
171
+
172
+ Example with a sort class (PostgreSQL JSONB text value):
173
+
174
+ ```ruby
175
+ class DataSort < Jsonapi::QueryBuilder::DynamicSort
176
+ def results
177
+ quoted_attribute = ActiveRecord::Base.connection.quote(dynamic_attribute)
178
+ collection.order(Arel.sql("(data->>#{quoted_attribute}) #{direction}"))
179
+ end
180
+ end
181
+
182
+ dynamically_sorts_by :'data.', DataSort
183
+ ```
121
184
 
122
185
  ### Filtering
123
186
 
124
187
  #### Simple exact match filters
188
+
125
189
  ```ruby
126
190
  filters_by :first_name
127
191
  # => collection.where(first_name: params.dig(:filter, :first_name)) if params.dig(:filter, :first_name).present?
128
192
  ```
129
193
 
130
194
  #### Lambda as a filter
195
+
131
196
  ```ruby
132
197
  filters_by :email, ->(collection, query) { collection.where('email ilike ?', "%#{query}%") }
133
198
  # => collection.where('email ilike ?', "%#{params.dig(:filter, :email)}%") if params.dig(:filter, :email).present?
134
199
  ```
135
200
 
136
201
  #### Filter classes
202
+
137
203
  We can define a filter class that responds to `#results` method, which returns the filtered collection results. Under
138
204
  the hood the filter class is initialized with the current scope and the query parameter. However, if the object responds
139
205
  to a `call` method it sends the current scope and the query parameter to that instead. This is great if you're using
@@ -142,17 +208,23 @@ query objects for ActiveRecord scopes, you can easily use them to filter with as
142
208
  ```ruby
143
209
  filters_by :type, TypeFilter
144
210
  ```
211
+
145
212
  The filter class could look something like
213
+
146
214
  ```ruby
215
+
147
216
  class TypeFilter < Jsonapi::QueryBuilder::BaseFilter
148
217
  def results
149
218
  collection.where(type: query.split(','))
150
219
  end
151
220
  end
152
221
  ```
222
+
153
223
  Sometimes you need to perform in-memory filtering, for example when database attributes are encrypted. In that case,
154
224
  those filters should be applied last, the order of definition in the query object matters.
225
+
155
226
  ```ruby
227
+
156
228
  class MrnFilter < Jsonapi::QueryBuilder::BaseFilter
157
229
  def results
158
230
  collection.select { |record| /#{query}/.match?(record.mrn) }
@@ -161,26 +233,34 @@ end
161
233
  ```
162
234
 
163
235
  #### Additional Options
236
+
164
237
  You can override the filter query parameter name by passing the `query_parameter` option.
238
+
165
239
  ```ruby
166
240
  filters_by :first_name, query_parameter: 'name'
167
241
  # => collection.where(first_name: params.dig(:filter, :name)) if params.dig(:filter, :name).present?
168
242
  ```
243
+
169
244
  `allow_nil` option changes the filter conditional to allow explicit checks for an attribute null value.
245
+
170
246
  ```ruby
171
247
  filters_by :first_name, allow_nil: true
172
248
  # => collection.where(first_name: params.dig(:filter, :first_name)) if params[:filter]&.key?(:first_name)
173
249
  ```
250
+
174
251
  The conditional when the filter is applied can also be defined explicitly. Note that these options override the
175
252
  `allow_nil` option, as the condition if defined explicitly and you should handle `nil` explicitly as well.
253
+
176
254
  ```ruby
177
255
  filters_by :first_name, if: ->(query) { query.length >= 2 }
178
256
  # => collection.where(first_name: params.dig(:filter, :first_name)) if params.dig(:filter, :first_name) >= 2
179
257
  filters_by :first_name, unless: ->(query) { query.length < 2 }
180
258
  # => collection.where(first_name: params.dig(:filter, :first_name)) unless params.dig(:filter, :first_name) < 2
181
259
  ```
260
+
182
261
  When you're using a filter class you can pass a symbol to the `:if` and `:unless` options which invokes the method on
183
262
  the filter class.
263
+
184
264
  ```ruby
185
265
  filters_by :type, TypeFilter, if: :correct_type?
186
266
  # => type_filter = TypeFilter.new(collection, query); type_filter.results if type_filter.correct_type?
@@ -202,7 +282,6 @@ version, push git commits and tags, and push the `.gem` file to [rubygems.org](h
202
282
 
203
283
  Bug reports and pull requests are welcome on GitHub at https://github.com/infinum/jsonapi-query_builder.
204
284
 
205
-
206
285
  ## License
207
286
 
208
287
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -36,9 +36,9 @@ Gem::Specification.new do |spec|
36
36
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
37
  spec.require_paths = ["lib"]
38
38
 
39
- spec.required_ruby_version = ">= 2.5"
39
+ spec.required_ruby_version = ">= 3.2"
40
40
 
41
- spec.add_runtime_dependency "activerecord", ">= 5"
41
+ spec.add_runtime_dependency "activerecord", ">= 7.2"
42
42
  spec.add_runtime_dependency "pagy", ">= 3.5"
43
43
 
44
44
  spec.add_development_dependency "bundler", "~> 2.0"
data/lefthook.yml CHANGED
@@ -2,7 +2,7 @@ lint:
2
2
  commands: &lint
3
3
  lint-frozen-strings:
4
4
  glob: "*.rb"
5
- run: bundle exec rubocop --only Style/FrozenStringLiteralComment,Layout/EmptyLineAfterMagicComment --format quiet --auto-correct
5
+ run: bundle exec rubocop --only Style/FrozenStringLiteralComment,Layout/EmptyLineAfterMagicComment --format quiet --autocorrect
6
6
 
7
7
  rubocop:
8
8
  commands: &rubocop
@@ -13,8 +13,7 @@ rubocop:
13
13
  pre-commit:
14
14
  parallel: true
15
15
  commands:
16
- <<: *lint
17
- <<: *rubocop
16
+ <<: [*lint, *rubocop]
18
17
  standardrb:
19
18
  glob: "*.rb"
20
19
  run: bundle exec standardrb {staged_files}
@@ -7,7 +7,7 @@ module Jsonapi
7
7
 
8
8
  # @param [ActiveRecord::Relation] collection
9
9
  # @param [Symbol] direction of the ordering, one of :asc or :desc
10
- def initialize(collection, direction)
10
+ def initialize(collection, direction = :asc)
11
11
  @collection = collection
12
12
  @direction = direction
13
13
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jsonapi/query_builder/base_sort"
4
+
5
+ module Jsonapi
6
+ module QueryBuilder
7
+ class DynamicSort < BaseSort
8
+ attr_reader :dynamic_attribute
9
+
10
+ # @param [ActiveRecord::Relation] collection
11
+ # @param [Symbol] dynamic_attribute, which attribute to dynamically sort by
12
+ # @param [Symbol] direction of the ordering, one of :asc or :desc
13
+ def initialize(collection, dynamic_attribute, direction = :asc)
14
+ super(collection, direction)
15
+ @dynamic_attribute = dynamic_attribute
16
+ end
17
+
18
+ # @return [ActiveRecord::Relation] Collection with order applied
19
+ def results
20
+ raise NotImplementedError, "#{self.class} should implement #results"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -5,11 +5,11 @@ module Jsonapi
5
5
  module Errors
6
6
  class UnpermittedSortParameters < ArgumentError
7
7
  def initialize(unpermitted_parameters)
8
- super [
8
+ super([
9
9
  unpermitted_parameters.to_sentence,
10
- unpermitted_parameters.count == 1 ? "is not a" : "are not",
10
+ (unpermitted_parameters.count == 1) ? "is not a" : "are not",
11
11
  "permitted sort attribute".pluralize(unpermitted_parameters.count)
12
- ].join(" ")
12
+ ].join(" "))
13
13
  end
14
14
  end
15
15
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jsonapi
4
+ module QueryBuilder
5
+ module Mixins
6
+ module Sort
7
+ class Dynamic
8
+ attr_reader :attribute_prefix, :sort
9
+
10
+ def initialize(attribute_prefix, sort)
11
+ @attribute_prefix = attribute_prefix.to_s
12
+ @sort = sort
13
+ end
14
+
15
+ def matches?(sort_attribute)
16
+ sort_attribute.to_s.start_with?(attribute_prefix)
17
+ end
18
+
19
+ def results(collection, sort_param)
20
+ dynamic_attribute = sort_param.attribute.delete_prefix(attribute_prefix)
21
+ if sort.respond_to?(:call)
22
+ sort.call(collection, dynamic_attribute, sort_param.direction)
23
+ else
24
+ sort.new(collection, dynamic_attribute, sort_param.direction).results
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jsonapi
4
+ module QueryBuilder
5
+ module Mixins
6
+ module Sort
7
+ class Static
8
+ attr_reader :attribute, :sort
9
+
10
+ def initialize(attribute, sort)
11
+ @attribute = attribute
12
+ @sort = sort || ->(collection, direction) { collection.order(attribute => direction) }
13
+ end
14
+
15
+ def results(collection, sort_param)
16
+ if sort.respond_to?(:call)
17
+ sort.call(collection, sort_param.direction)
18
+ else
19
+ sort.new(collection, sort_param.direction).results
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "jsonapi/query_builder/mixins/sort/param"
4
+ require "jsonapi/query_builder/mixins/sort/static"
5
+ require "jsonapi/query_builder/mixins/sort/dynamic"
4
6
  require "jsonapi/query_builder/errors/unpermitted_sort_parameters"
5
7
 
6
8
  module Jsonapi
@@ -16,8 +18,14 @@ module Jsonapi
16
18
  @_unique_sort_attributes || [id: :asc]
17
19
  end
18
20
 
21
+ # @return [Hash<Symbol, Jsonapi::QueryBuilder::Mixins::Sort::Static>] Supported sorts
19
22
  def supported_sorts
20
- @supported_sorts || {}
23
+ @supported_sorts ||= {}
24
+ end
25
+
26
+ # @return [Array<Jsonapi::QueryBuilder::Mixins::Sort::Dynamic>] Supported dynamic sorts
27
+ def supported_dynamic_sorts
28
+ @supported_dynamic_sorts ||= []
21
29
  end
22
30
 
23
31
  # Ensures deterministic ordering. Defaults to :id in ascending direction.
@@ -43,8 +51,14 @@ module Jsonapi
43
51
  # @param [Symbol] attribute The "sortable" attribute
44
52
  # @param [proc, Class] sort A proc or a sort class, defaults to a simple order(attribute => direction)
45
53
  def sorts_by(attribute, sort = nil)
46
- sort ||= ->(collection, direction) { collection.order(attribute => direction) }
47
- @supported_sorts = {**supported_sorts, attribute => sort}
54
+ supported_sorts[attribute] = Sort::Static.new(attribute, sort)
55
+ end
56
+
57
+ # Registers an attribute prefix that can be dynamically used for sorting. Attribute prefix is stripped from parsed sort parameter and passed to the sort proc or class.
58
+ # @param [Symbol] attribute_prefix The "sortable" attribute prefix, e.g. `:'data.'` for sorting by `data.name` and `data.created_at`
59
+ # @param [proc, Class] sort A proc or a sort class, defaults to a simple order(attribute => direction)
60
+ def dynamically_sorts_by(attribute_prefix, sort)
61
+ supported_dynamic_sorts << Sort::Dynamic.new(attribute_prefix, sort)
48
62
  end
49
63
  end
50
64
 
@@ -76,7 +90,9 @@ module Jsonapi
76
90
  end
77
91
 
78
92
  def ensure_permitted_sort_params!(sort_params)
79
- unpermitted_parameters = sort_params.map(&:attribute).map(&:to_sym) - self.class.supported_sorts.keys
93
+ unpermitted_parameters = sort_params.map(&:attribute).filter do |attribute|
94
+ !self.class.supported_sorts.key?(attribute.to_sym) && self.class.supported_dynamic_sorts.none? { |dynamic_sort| dynamic_sort.matches?(attribute) }
95
+ end
80
96
  return if unpermitted_parameters.size.zero?
81
97
 
82
98
  raise Errors::UnpermittedSortParameters, unpermitted_parameters
@@ -84,16 +100,26 @@ module Jsonapi
84
100
 
85
101
  def add_order_attributes(collection, sort_params)
86
102
  return collection if self.class._default_sort.nil? && sort_params.blank?
87
- return collection.order(self.class._default_sort) if sort_params.blank?
103
+ return sort_by_default(collection) if sort_params.blank?
88
104
 
89
105
  sort_params.reduce(collection) do |sorted_collection, sort_param|
90
- sort = self.class.supported_sorts.fetch(sort_param.attribute.to_sym)
91
-
92
- if sort.respond_to?(:call)
93
- sort.call(sorted_collection, sort_param.direction)
94
- else
95
- sort.new(sorted_collection, sort_param.direction).results
106
+ sort = self.class.supported_sorts.fetch(sort_param.attribute.to_sym) do
107
+ self.class.supported_dynamic_sorts.find { |dynamic_sort| dynamic_sort.matches?(sort_param.attribute) }
96
108
  end
109
+
110
+ sort.results(sorted_collection, sort_param)
111
+ end
112
+ end
113
+
114
+ def sort_by_default(collection)
115
+ default_sort = self.class._default_sort
116
+
117
+ if default_sort.is_a?(Symbol) || default_sort.is_a?(Hash)
118
+ collection.order(default_sort)
119
+ elsif default_sort.respond_to?(:call)
120
+ default_sort.call(collection)
121
+ else
122
+ default_sort.new(collection).results
97
123
  end
98
124
  end
99
125
 
@@ -12,9 +12,8 @@ module Jsonapi
12
12
  def paginate(page_params)
13
13
  @params = {page: page_params}
14
14
 
15
- pagination_details, records = pagy collection, page: page_params[:number],
16
- items: page_params[:size],
17
- outset: page_params[:offset]
15
+ pagination_details, records = pagy(collection, page: page_params[:number], items: page_params[:size], outset: page_params[:offset])
16
+
18
17
  [records, pagination_details]
19
18
  end
20
19
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pagy"
4
+ require "pagy/extras/items"
5
+ require "pagy/extras/countless"
6
+
7
+ module Jsonapi
8
+ module QueryBuilder
9
+ module Paginator
10
+ class PagyCountless < BasePaginator
11
+ include ::Pagy::Backend
12
+
13
+ def paginate(page_params)
14
+ @params = {page: page_params}
15
+
16
+ pagination_details, records = pagy_countless(collection, page: page_params[:number],
17
+ items: page_params[:size],
18
+ outset: page_params[:offset])
19
+
20
+ [records, pagination_details]
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :params
26
+ end
27
+ end
28
+ end
29
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jsonapi
4
4
  module QueryBuilder
5
- VERSION = "0.2.1"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
@@ -9,3 +9,4 @@ require "jsonapi/query_builder/version"
9
9
  require "jsonapi/query_builder/base_query"
10
10
  require "jsonapi/query_builder/base_filter"
11
11
  require "jsonapi/query_builder/base_sort"
12
+ require "jsonapi/query_builder/dynamic_sort"
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsonapi-query_builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jure Cindro
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2021-10-04 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activerecord
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '5'
18
+ version: '7.2'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '5'
25
+ version: '7.2'
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: pagy
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -236,17 +235,21 @@ files:
236
235
  - lib/jsonapi/query_builder/base_filter.rb
237
236
  - lib/jsonapi/query_builder/base_query.rb
238
237
  - lib/jsonapi/query_builder/base_sort.rb
238
+ - lib/jsonapi/query_builder/dynamic_sort.rb
239
239
  - lib/jsonapi/query_builder/errors/unpermitted_sort_parameters.rb
240
240
  - lib/jsonapi/query_builder/mixins/filter.rb
241
241
  - lib/jsonapi/query_builder/mixins/include.rb
242
242
  - lib/jsonapi/query_builder/mixins/paginate.rb
243
243
  - lib/jsonapi/query_builder/mixins/sort.rb
244
+ - lib/jsonapi/query_builder/mixins/sort/dynamic.rb
244
245
  - lib/jsonapi/query_builder/mixins/sort/param.rb
246
+ - lib/jsonapi/query_builder/mixins/sort/static.rb
245
247
  - lib/jsonapi/query_builder/paginator.rb
246
248
  - lib/jsonapi/query_builder/paginator/base_paginator.rb
247
249
  - lib/jsonapi/query_builder/paginator/kaminari.rb
248
250
  - lib/jsonapi/query_builder/paginator/keyset.rb
249
251
  - lib/jsonapi/query_builder/paginator/pagy.rb
252
+ - lib/jsonapi/query_builder/paginator/pagy_countless.rb
250
253
  - lib/jsonapi/query_builder/version.rb
251
254
  homepage: https://github.com/infinum/jsonapi-query_builder
252
255
  licenses:
@@ -255,7 +258,6 @@ metadata:
255
258
  homepage_uri: https://github.com/infinum/jsonapi-query_builder
256
259
  source_code_uri: https://github.com/infinum/jsonapi-query_builder
257
260
  changelog_uri: https://github.com/infinum/jsonapi-query_builder/blob/master/CHANGELOG.md
258
- post_install_message:
259
261
  rdoc_options: []
260
262
  require_paths:
261
263
  - lib
@@ -263,15 +265,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
263
265
  requirements:
264
266
  - - ">="
265
267
  - !ruby/object:Gem::Version
266
- version: '2.5'
268
+ version: '3.2'
267
269
  required_rubygems_version: !ruby/object:Gem::Requirement
268
270
  requirements:
269
271
  - - ">="
270
272
  - !ruby/object:Gem::Version
271
273
  version: '0'
272
274
  requirements: []
273
- rubygems_version: 3.2.22
274
- signing_key:
275
+ rubygems_version: 3.6.9
275
276
  specification_version: 4
276
277
  summary: Support `json:api` querying with ease!
277
278
  test_files: []