jsonapi-query_builder 0.1.6.pre → 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 +4 -4
- data/.github/workflows/lint.yml +31 -0
- data/.github/workflows/spec.yml +23 -0
- data/.gitignore +0 -1
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +16 -0
- data/Gemfile.lock +135 -0
- data/README.md +56 -12
- data/jsonapi-query_builder.gemspec +7 -2
- data/lefthook.yml +2 -2
- data/lib/jsonapi/query_builder/base_filter.rb +3 -0
- data/lib/jsonapi/query_builder/base_query.rb +20 -4
- data/lib/jsonapi/query_builder/base_sort.rb +21 -0
- data/lib/jsonapi/query_builder/errors/unpermitted_sort_parameters.rb +17 -0
- data/lib/jsonapi/query_builder/mixins/filter.rb +104 -0
- data/lib/jsonapi/query_builder/mixins/include.rb +5 -0
- data/lib/jsonapi/query_builder/mixins/paginate.rb +20 -4
- data/lib/jsonapi/query_builder/mixins/sort/param.rb +37 -0
- data/lib/jsonapi/query_builder/mixins/sort.rb +58 -25
- data/lib/jsonapi/query_builder/paginator/base_paginator.rb +22 -0
- data/lib/jsonapi/query_builder/paginator/kaminari.rb +34 -0
- data/lib/jsonapi/query_builder/paginator/keyset.rb +64 -0
- data/lib/jsonapi/query_builder/paginator/pagy.rb +27 -0
- data/lib/jsonapi/query_builder/paginator.rb +4 -0
- data/lib/jsonapi/query_builder/version.rb +1 -1
- data/lib/jsonapi/query_builder.rb +1 -2
- metadata +87 -12
- data/lib/jsonapi/query_builder/mixins/filtering.rb +0 -64
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51c9f4a8cc7fb70d6ca5a535cdc3386e97231960b915b02af356126b57d80609
|
|
4
|
+
data.tar.gz: 9a1684797aa2b39a67c66226da08c0adf326133e4d51190a78f34817e4fc288b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70121ffb0d3419bfddff71778b6d4214e4de00c9f61f0bbd272ff6ab5f0a9e848f04c37124cc911371909326fa1f1a41cf1e401f2cd4dcc56e309cc323e2b9fd
|
|
7
|
+
data.tar.gz: ed296c71cf3bfdc1b46ee995fad2c2de965b38bafad1cad00f8571e55e09a83f18627b36360e231f68e9015b211fcefde2686ba8ef41bdce8c4060dc6fc32506
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: lint
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ master ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ master ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
standardrb:
|
|
11
|
+
runs-on: ubuntu-18.04
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v2
|
|
15
|
+
- uses: ruby/setup-ruby@v1
|
|
16
|
+
with:
|
|
17
|
+
bundler-cache: true
|
|
18
|
+
- name: standardrb
|
|
19
|
+
run: bundle exec standardrb
|
|
20
|
+
|
|
21
|
+
rubocop-rspec:
|
|
22
|
+
runs-on: ubuntu-18.04
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v2
|
|
26
|
+
- uses: ruby/setup-ruby@v1
|
|
27
|
+
with:
|
|
28
|
+
bundler-cache: true
|
|
29
|
+
|
|
30
|
+
- name: rubocop
|
|
31
|
+
run: bundle exec rubocop --only RSpec
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: spec
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ master ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ master ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
rake-spec:
|
|
11
|
+
runs-on: ubuntu-18.04
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
ruby: [2.5, 2.6, 2.7, 3.0]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v2
|
|
18
|
+
- uses: ruby/setup-ruby@v1
|
|
19
|
+
with:
|
|
20
|
+
ruby-version: ${{ matrix.ruby }}
|
|
21
|
+
bundler-cache: true
|
|
22
|
+
- name: rake spec
|
|
23
|
+
run: bundle exec rake spec
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Change log
|
|
2
|
+
|
|
3
|
+
## 0.2.0 (2021-09-29)
|
|
4
|
+
Added support for Kaminari and Keyset pagination strategies in addition to Pagy.
|
|
5
|
+
|
|
6
|
+
- [#21](https://github.com/infinum/jsonapi-query_builder/pull/21): Extract paginators.
|
|
7
|
+
|
|
8
|
+
## 0.1.9 (2021-05-07)
|
|
9
|
+
|
|
10
|
+
- [#18](https://github.com/infinum/jsonapi-query_builder/pull/18): Remove Ruby `to` version.
|
|
11
|
+
- [#9](https://github.com/infinum/jsonapi-query_builder/pull/9): added github actions
|
|
12
|
+
|
|
13
|
+
## 0.1.8 (2021-01-25)
|
|
14
|
+
|
|
15
|
+
- [#8](https://github.com/infinum/jsonapi-query_builder/pull/8): add support for ruby 3.0
|
|
16
|
+
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
jsonapi-query_builder (0.2.0)
|
|
5
|
+
activerecord (>= 5)
|
|
6
|
+
pagy (~> 3.5)
|
|
7
|
+
|
|
8
|
+
GEM
|
|
9
|
+
remote: https://rubygems.org/
|
|
10
|
+
specs:
|
|
11
|
+
actionview (6.1.4.1)
|
|
12
|
+
activesupport (= 6.1.4.1)
|
|
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)
|
|
24
|
+
i18n (>= 1.6, < 2)
|
|
25
|
+
minitest (>= 5.1)
|
|
26
|
+
tzinfo (~> 2.0)
|
|
27
|
+
zeitwerk (~> 2.3)
|
|
28
|
+
ast (2.4.2)
|
|
29
|
+
builder (3.2.4)
|
|
30
|
+
coderay (1.1.3)
|
|
31
|
+
concurrent-ruby (1.1.9)
|
|
32
|
+
crass (1.0.6)
|
|
33
|
+
diff-lcs (1.4.4)
|
|
34
|
+
erubi (1.10.0)
|
|
35
|
+
i18n (1.8.10)
|
|
36
|
+
concurrent-ruby (~> 1.0)
|
|
37
|
+
kaminari (1.2.1)
|
|
38
|
+
activesupport (>= 4.1.0)
|
|
39
|
+
kaminari-actionview (= 1.2.1)
|
|
40
|
+
kaminari-activerecord (= 1.2.1)
|
|
41
|
+
kaminari-core (= 1.2.1)
|
|
42
|
+
kaminari-actionview (1.2.1)
|
|
43
|
+
actionview
|
|
44
|
+
kaminari-core (= 1.2.1)
|
|
45
|
+
kaminari-activerecord (1.2.1)
|
|
46
|
+
activerecord
|
|
47
|
+
kaminari-core (= 1.2.1)
|
|
48
|
+
kaminari-core (1.2.1)
|
|
49
|
+
lefthook (0.7.6)
|
|
50
|
+
loofah (2.12.0)
|
|
51
|
+
crass (~> 1.0.2)
|
|
52
|
+
nokogiri (>= 1.5.9)
|
|
53
|
+
method_source (1.0.0)
|
|
54
|
+
mini_portile2 (2.6.1)
|
|
55
|
+
minitest (5.14.4)
|
|
56
|
+
nokogiri (1.12.5)
|
|
57
|
+
mini_portile2 (~> 2.6.1)
|
|
58
|
+
racc (~> 1.4)
|
|
59
|
+
pagy (3.14.0)
|
|
60
|
+
parallel (1.21.0)
|
|
61
|
+
parser (3.0.2.0)
|
|
62
|
+
ast (~> 2.4.1)
|
|
63
|
+
pry (0.14.1)
|
|
64
|
+
coderay (~> 1.1)
|
|
65
|
+
method_source (~> 1.0)
|
|
66
|
+
racc (1.5.2)
|
|
67
|
+
rails-dom-testing (2.0.3)
|
|
68
|
+
activesupport (>= 4.2.0)
|
|
69
|
+
nokogiri (>= 1.6)
|
|
70
|
+
rails-html-sanitizer (1.4.2)
|
|
71
|
+
loofah (~> 2.3)
|
|
72
|
+
rainbow (3.0.0)
|
|
73
|
+
rake (13.0.6)
|
|
74
|
+
regexp_parser (2.1.1)
|
|
75
|
+
rexml (3.2.5)
|
|
76
|
+
rspec (3.10.0)
|
|
77
|
+
rspec-core (~> 3.10.0)
|
|
78
|
+
rspec-expectations (~> 3.10.0)
|
|
79
|
+
rspec-mocks (~> 3.10.0)
|
|
80
|
+
rspec-core (3.10.1)
|
|
81
|
+
rspec-support (~> 3.10.0)
|
|
82
|
+
rspec-expectations (3.10.1)
|
|
83
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
84
|
+
rspec-support (~> 3.10.0)
|
|
85
|
+
rspec-mocks (3.10.2)
|
|
86
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
87
|
+
rspec-support (~> 3.10.0)
|
|
88
|
+
rspec-support (3.10.2)
|
|
89
|
+
rubocop (1.20.0)
|
|
90
|
+
parallel (~> 1.10)
|
|
91
|
+
parser (>= 3.0.0.0)
|
|
92
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
93
|
+
regexp_parser (>= 1.8, < 3.0)
|
|
94
|
+
rexml
|
|
95
|
+
rubocop-ast (>= 1.9.1, < 2.0)
|
|
96
|
+
ruby-progressbar (~> 1.7)
|
|
97
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
|
98
|
+
rubocop-ast (1.12.0)
|
|
99
|
+
parser (>= 3.0.1.1)
|
|
100
|
+
rubocop-performance (1.11.5)
|
|
101
|
+
rubocop (>= 1.7.0, < 2.0)
|
|
102
|
+
rubocop-ast (>= 0.4.0)
|
|
103
|
+
rubocop-rspec (2.5.0)
|
|
104
|
+
rubocop (~> 1.19)
|
|
105
|
+
ruby-progressbar (1.11.0)
|
|
106
|
+
sqlite3 (1.4.2)
|
|
107
|
+
standard (1.3.0)
|
|
108
|
+
rubocop (= 1.20.0)
|
|
109
|
+
rubocop-performance (= 1.11.5)
|
|
110
|
+
standardrb (1.0.0)
|
|
111
|
+
standard
|
|
112
|
+
tzinfo (2.0.4)
|
|
113
|
+
concurrent-ruby (~> 1.0)
|
|
114
|
+
unicode-display_width (2.1.0)
|
|
115
|
+
zeitwerk (2.4.2)
|
|
116
|
+
|
|
117
|
+
PLATFORMS
|
|
118
|
+
ruby
|
|
119
|
+
|
|
120
|
+
DEPENDENCIES
|
|
121
|
+
activerecord
|
|
122
|
+
bundler (~> 2.0)
|
|
123
|
+
jsonapi-query_builder!
|
|
124
|
+
kaminari (~> 1.2)
|
|
125
|
+
lefthook
|
|
126
|
+
pry
|
|
127
|
+
rake (~> 13.0)
|
|
128
|
+
rspec (~> 3.0)
|
|
129
|
+
rubocop-rspec
|
|
130
|
+
sqlite3
|
|
131
|
+
standard
|
|
132
|
+
standardrb
|
|
133
|
+
|
|
134
|
+
BUNDLED WITH
|
|
135
|
+
2.1.4
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Jsonapi::QueryBuilder
|
|
1
|
+
# Jsonapi::QueryBuilder 
|
|
2
2
|
|
|
3
3
|
`Jsonapi::QueryBuilder` serves the purpose of adding the json api query related SQL conditions to the already scoped
|
|
4
4
|
collection, usually used in controller index actions.
|
|
@@ -27,9 +27,14 @@ Or install it yourself as:
|
|
|
27
27
|
|
|
28
28
|
```ruby
|
|
29
29
|
class UserQuery < Jsonapi::QueryBuilder::BaseQuery
|
|
30
|
+
## pagination
|
|
31
|
+
paginator Jsonapi::QueryBuilder::Paginator::Pagy # default paginator
|
|
32
|
+
|
|
30
33
|
## sorting
|
|
31
34
|
default_sort created_at: :desc
|
|
32
|
-
sorts_by :
|
|
35
|
+
sorts_by :last_name
|
|
36
|
+
sorts_by :first_name, ->(collection, direction) { collection.order(name: direction) }
|
|
37
|
+
sorts_by :email, EmailSort
|
|
33
38
|
|
|
34
39
|
## filtering
|
|
35
40
|
filters_by :first_name
|
|
@@ -54,6 +59,29 @@ current user permissions, or for any other type of scoping. It's only responsibi
|
|
|
54
59
|
querying. Use `pundit` or similar for policy scoping, custom query objects for other scoping, and then pass the scoped
|
|
55
60
|
collection to the `Jsonapi::QueryBuilder::BaseQuery` object.
|
|
56
61
|
|
|
62
|
+
### Pagination
|
|
63
|
+
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.
|
|
71
|
+
#### Using the Kaminari Paginator
|
|
72
|
+
```ruby
|
|
73
|
+
require "jsonapi/query_builder/paginator/kaminari"
|
|
74
|
+
|
|
75
|
+
paginator Jsonapi::QueryBuilder::Paginator::Kaminari
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### Using the Keyset Paginator
|
|
79
|
+
```ruby
|
|
80
|
+
require "jsonapi/query_builder/paginator/keyset"
|
|
81
|
+
|
|
82
|
+
paginator Jsonapi::QueryBuilder::Paginator::Keyset
|
|
83
|
+
```
|
|
84
|
+
|
|
57
85
|
### Sorting
|
|
58
86
|
#### Ensuring deterministic results
|
|
59
87
|
Sorting has a fallback to an unique attribute which defaults to the `id` attribute. This ensures deterministic paginated
|
|
@@ -71,14 +99,26 @@ are passed directly to the underlying active record relation, so the usual order
|
|
|
71
99
|
```ruby
|
|
72
100
|
default_sort created_at: :desc
|
|
73
101
|
```
|
|
74
|
-
#### Enabling sorting for attributes
|
|
102
|
+
#### Enabling simple sorting for attributes
|
|
75
103
|
`sorts_by` denotes which attributes can be used for sorting. Sorting parameters are usually parsed from the
|
|
76
|
-
`json:api` sort query parameter in order they are given. So `sort=-first_name,email` would translate to
|
|
77
|
-
`{ first_name: :desc, email: :asc }`
|
|
104
|
+
`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 }`
|
|
106
|
+
```ruby
|
|
107
|
+
sorts_by :first_name
|
|
108
|
+
sorts_by :email
|
|
109
|
+
```
|
|
110
|
+
#### Sorting with lambdas
|
|
111
|
+
`sorts_by` also supports passing a lambda to implement a custom order or reorder function. The parameters passed to the
|
|
112
|
+
lamdba are collection and the direction of the order, which is either `:desc` or `:asc`.
|
|
78
113
|
```ruby
|
|
79
|
-
sorts_by :first_name, :
|
|
114
|
+
sorts_by :first_name, ->(collection, direction) { collection.order(name: direction) }
|
|
80
115
|
```
|
|
81
116
|
|
|
117
|
+
#### 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.
|
|
121
|
+
|
|
82
122
|
### Filtering
|
|
83
123
|
|
|
84
124
|
#### Simple exact match filters
|
|
@@ -94,10 +134,11 @@ filters_by :email, ->(collection, query) { collection.where('email ilike ?', "%#
|
|
|
94
134
|
```
|
|
95
135
|
|
|
96
136
|
#### Filter classes
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
the current scope and the query parameter
|
|
100
|
-
ActiveRecord scopes, you can easily use them to filter as well.
|
|
137
|
+
We can define a filter class that responds to `#results` method, which returns the filtered collection results. Under
|
|
138
|
+
the hood the filter class is initialized with the current scope and the query parameter. However, if the object responds
|
|
139
|
+
to a `call` method it sends the current scope and the query parameter to that instead. This is great if you're using
|
|
140
|
+
query objects for ActiveRecord scopes, you can easily use them to filter with as well.
|
|
141
|
+
|
|
101
142
|
```ruby
|
|
102
143
|
filters_by :type, TypeFilter
|
|
103
144
|
```
|
|
@@ -150,9 +191,12 @@ filters_by :type, TypeFilter, if: :correct_type?
|
|
|
150
191
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can
|
|
151
192
|
also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
152
193
|
|
|
194
|
+
We're using `standardrb` and `lefthook`. You can install lefthook hooks via `lefthook install`. It will run linters and
|
|
195
|
+
standardrb checks before commits, and a bundle audit + whole spec suite before push.
|
|
196
|
+
|
|
153
197
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
|
|
154
|
-
version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the
|
|
155
|
-
push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
198
|
+
version number in `version.rb`, and then run `LEFTHOOK=0 bundle exec rake release`, which will create a git tag for the
|
|
199
|
+
version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
156
200
|
|
|
157
201
|
## Contributing
|
|
158
202
|
|
|
@@ -36,15 +36,20 @@ 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 = "
|
|
39
|
+
spec.required_ruby_version = ">= 2.5"
|
|
40
40
|
|
|
41
|
-
spec.add_runtime_dependency "activerecord", ">= 5"
|
|
41
|
+
spec.add_runtime_dependency "activerecord", ">= 5"
|
|
42
42
|
spec.add_runtime_dependency "pagy", "~> 3.5"
|
|
43
43
|
|
|
44
44
|
spec.add_development_dependency "bundler", "~> 2.0"
|
|
45
45
|
spec.add_development_dependency "rake", "~> 13.0"
|
|
46
46
|
spec.add_development_dependency "rspec", "~> 3.0"
|
|
47
47
|
spec.add_development_dependency "standardrb"
|
|
48
|
+
spec.add_development_dependency "standard"
|
|
48
49
|
spec.add_development_dependency "rubocop-rspec"
|
|
49
50
|
spec.add_development_dependency "lefthook"
|
|
51
|
+
spec.add_development_dependency "kaminari", "~> 1.2"
|
|
52
|
+
spec.add_development_dependency "activerecord"
|
|
53
|
+
spec.add_development_dependency "sqlite3"
|
|
54
|
+
spec.add_development_dependency "pry"
|
|
50
55
|
end
|
data/lefthook.yml
CHANGED
|
@@ -2,13 +2,13 @@ lint:
|
|
|
2
2
|
commands: &lint
|
|
3
3
|
lint-frozen-strings:
|
|
4
4
|
glob: "*.rb"
|
|
5
|
-
run: bundle exec rubocop
|
|
5
|
+
run: bundle exec rubocop --only Style/FrozenStringLiteralComment,Layout/EmptyLineAfterMagicComment --format quiet --auto-correct
|
|
6
6
|
|
|
7
7
|
rubocop:
|
|
8
8
|
commands: &rubocop
|
|
9
9
|
rubocop-rspec:
|
|
10
10
|
glob: "spec/*"
|
|
11
|
-
run: bundle exec rubocop
|
|
11
|
+
run: bundle exec rubocop --only RSpec --format quiet
|
|
12
12
|
|
|
13
13
|
pre-commit:
|
|
14
14
|
parallel: true
|
|
@@ -5,11 +5,14 @@ module Jsonapi
|
|
|
5
5
|
class BaseFilter
|
|
6
6
|
attr_reader :collection, :query
|
|
7
7
|
|
|
8
|
+
# @param [ActiveRecord::Relation] collection
|
|
9
|
+
# @param [String] query the query value used for filtering
|
|
8
10
|
def initialize(collection, query)
|
|
9
11
|
@collection = collection
|
|
10
12
|
@query = query
|
|
11
13
|
end
|
|
12
14
|
|
|
15
|
+
# @return [ActiveRecord::Relation] Collection with the filter applied
|
|
13
16
|
def results
|
|
14
17
|
raise NotImplementedError, "#{self.class} should implement #results"
|
|
15
18
|
end
|
|
@@ -1,41 +1,57 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "jsonapi/query_builder/mixins/
|
|
3
|
+
require "jsonapi/query_builder/mixins/filter"
|
|
4
4
|
require "jsonapi/query_builder/mixins/include"
|
|
5
5
|
require "jsonapi/query_builder/mixins/paginate"
|
|
6
6
|
require "jsonapi/query_builder/mixins/sort"
|
|
7
7
|
|
|
8
|
+
require "jsonapi/query_builder/paginator"
|
|
9
|
+
|
|
8
10
|
module Jsonapi
|
|
9
11
|
module QueryBuilder
|
|
10
12
|
class BaseQuery
|
|
11
|
-
include Mixins::
|
|
13
|
+
include Mixins::Filter
|
|
12
14
|
include Mixins::Include
|
|
13
15
|
include Mixins::Paginate
|
|
14
16
|
include Mixins::Sort
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
attr_accessor :collection, :params
|
|
17
19
|
|
|
20
|
+
# @param [ActiveRecord::Relation] collection
|
|
21
|
+
# @param [Hash] params Json:Api query parameters
|
|
18
22
|
def initialize(collection, params)
|
|
19
23
|
@collection = collection
|
|
20
24
|
@params = params.deep_symbolize_keys
|
|
21
25
|
end
|
|
22
26
|
|
|
27
|
+
# @return [ActiveRecord::Relation] A collection with eager loaded relationships based on include params, filtered,
|
|
28
|
+
# ordered and lastly, paginated.
|
|
29
|
+
# @note Pagination details are saved to an instance variable and can be accessed via the #pagination_details attribute reader
|
|
23
30
|
def results
|
|
24
31
|
collection
|
|
25
|
-
.yield_self(&method(:sort))
|
|
26
32
|
.yield_self(&method(:add_includes))
|
|
33
|
+
.yield_self(&method(:sort))
|
|
27
34
|
.yield_self(&method(:filter))
|
|
28
35
|
.yield_self(&method(:paginate))
|
|
29
36
|
end
|
|
30
37
|
|
|
38
|
+
# @param [integer, string] id
|
|
39
|
+
# @return [Object]
|
|
40
|
+
# @raise [ActiveRecord::RecordNotFound] if the record by the id is not found
|
|
31
41
|
def find(id)
|
|
32
42
|
find_by! id: id
|
|
33
43
|
end
|
|
34
44
|
|
|
45
|
+
# Finds the record by the id parameter the class is instantiated with
|
|
46
|
+
# @return (see #find)
|
|
47
|
+
# @raise (see #find)
|
|
35
48
|
def record
|
|
36
49
|
find_by! id: params[:id]
|
|
37
50
|
end
|
|
38
51
|
|
|
52
|
+
# @param [Hash] kwargs Attributes with required values
|
|
53
|
+
# @return (see #find)
|
|
54
|
+
# @raise [ActiveRecord::RecordNotFound] if the record by the arguments is not found
|
|
39
55
|
def find_by!(**kwargs)
|
|
40
56
|
add_includes(collection).find_by!(kwargs)
|
|
41
57
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jsonapi
|
|
4
|
+
module QueryBuilder
|
|
5
|
+
class BaseSort
|
|
6
|
+
attr_reader :collection, :direction
|
|
7
|
+
|
|
8
|
+
# @param [ActiveRecord::Relation] collection
|
|
9
|
+
# @param [Symbol] direction of the ordering, one of :asc or :desc
|
|
10
|
+
def initialize(collection, direction)
|
|
11
|
+
@collection = collection
|
|
12
|
+
@direction = direction
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [ActiveRecord::Relation] Collection with order applied
|
|
16
|
+
def results
|
|
17
|
+
raise NotImplementedError, "#{self.class} should implement #results"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jsonapi
|
|
4
|
+
module QueryBuilder
|
|
5
|
+
module Errors
|
|
6
|
+
class UnpermittedSortParameters < ArgumentError
|
|
7
|
+
def initialize(unpermitted_parameters)
|
|
8
|
+
super [
|
|
9
|
+
unpermitted_parameters.to_sentence,
|
|
10
|
+
unpermitted_parameters.count == 1 ? "is not a" : "are not",
|
|
11
|
+
"permitted sort attribute".pluralize(unpermitted_parameters.count)
|
|
12
|
+
].join(" ")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jsonapi
|
|
4
|
+
module QueryBuilder
|
|
5
|
+
module Mixins
|
|
6
|
+
module Filter
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
def supported_filters
|
|
11
|
+
@supported_filters || {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Registers an attribute to filter by. Filters are applied in the order they are registered, if they are
|
|
15
|
+
# applicable, so in-memory filters should be registered last.
|
|
16
|
+
#
|
|
17
|
+
# @param [Symbol] attribute The attribute which can be used to filter by
|
|
18
|
+
# @param [proc, Class] filter A proc or a filter class, defaults to a simple where(attribute => query)
|
|
19
|
+
# @param [Hash] options Additional filter options
|
|
20
|
+
# @option options [Symbol] :query_parameter used to override the filter query parameter name
|
|
21
|
+
# @option options [Boolean] :allow_nil changes the filter conditional to allow explicit checks for an
|
|
22
|
+
# attribute null value
|
|
23
|
+
# @option options [proc, Symbol] :if Define the conditional for applying the filter. If passed a symbol, a
|
|
24
|
+
# method with that name is invoked on the instantiated filter object
|
|
25
|
+
# @option options [proc, Symbol] :unless Define the conditional for applying the filter. If passed a symbol, a
|
|
26
|
+
# method with that name is invoked on the instantiated filter object
|
|
27
|
+
#
|
|
28
|
+
# @example Change the query parameter name
|
|
29
|
+
# filters_by :first_name, query_parameter: 'name'
|
|
30
|
+
# # => collection.where(first_name: params.dig(:filter, :name)) if params.dig(:filter, :name).present?
|
|
31
|
+
#
|
|
32
|
+
# @example Allow checks for null values
|
|
33
|
+
# filters_by :first_name, allow_nil: true
|
|
34
|
+
# # => collection.where(first_name: params.dig(:filter, :first_name)) if params[:filter]&.key?(:first_name)
|
|
35
|
+
#
|
|
36
|
+
# @example Change the filter condition
|
|
37
|
+
# filters_by :first_name, if: ->(query) { query.length >= 2 }
|
|
38
|
+
# # => collection.where(first_name: params.dig(:filter, :first_name)) if params.dig(:filter, :first_name) >= 2
|
|
39
|
+
# filters_by :first_name, unless: ->(query) { query.length < 2 }
|
|
40
|
+
# # => collection.where(first_name: params.dig(:filter, :first_name)) unless params.dig(:filter, :first_name) < 2
|
|
41
|
+
# filters_by :type, TypeFilter, if: :correct_type?
|
|
42
|
+
# # => TypeFilter.new(collection, query).yield_self { |filter| filter.results if filter.correct_type? }
|
|
43
|
+
def filters_by(attribute, filter = nil, **options)
|
|
44
|
+
filter ||= ->(collection, query) { collection.where(attribute => query) }
|
|
45
|
+
@supported_filters = {**supported_filters, attribute => [filter, options]}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Filters the passed relation with the default filter params (parsed from the queries params) or with explicitly
|
|
50
|
+
# passed filter parameters.
|
|
51
|
+
# Iterates through registered filters so that the filter application order is settable from the backend side
|
|
52
|
+
# instead of being dependent on the query order from the clients. If the filter condition for the filter
|
|
53
|
+
# strategy is met, then the filter is applied to the collection. If the strategy responds to a call method it
|
|
54
|
+
# calls it with the collection and parameter's parsed sort direction, otherwise it instantiates the filter class
|
|
55
|
+
# with the collection and the parameter's query value and calls for the results.
|
|
56
|
+
# @param [ActiveRecord::Relation] collection
|
|
57
|
+
# @param [Object] filter_params Optional explicit filter params
|
|
58
|
+
# @return [ActiveRecord::Relation, Array] An AR relation is returned unless filters need to resort to in-memory
|
|
59
|
+
# filtering strategy, then an array is returned.
|
|
60
|
+
def filter(collection, filter_params = send(:filter_params))
|
|
61
|
+
self.class.supported_filters.reduce(collection) do |filtered_collection, supported_filter|
|
|
62
|
+
filter, options = serialize_filter(supported_filter, collection: filtered_collection, params: filter_params)
|
|
63
|
+
|
|
64
|
+
next filtered_collection unless options[:conditions].all? { |type, condition|
|
|
65
|
+
check_condition(condition, type, filter: filter, query: options[:query])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
filter.respond_to?(:call) ? filter.call(filtered_collection, options[:query]) : filter.results
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def filter_params
|
|
75
|
+
params[:filter] || {}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def serialize_filter(supported_filter, collection:, params:)
|
|
79
|
+
attribute, (filter, options) = *supported_filter
|
|
80
|
+
|
|
81
|
+
options[:query_parameter] = options[:query_parameter]&.to_sym || attribute
|
|
82
|
+
options[:query] = params[options[:query_parameter]].presence
|
|
83
|
+
options[:conditions] = options.slice(:if, :unless).presence ||
|
|
84
|
+
{if: options[:query].present? || options[:allow_nil] && params.key?(options[:query_parameter])}
|
|
85
|
+
|
|
86
|
+
filter = filter.new(collection, options[:query]) unless filter.respond_to?(:call)
|
|
87
|
+
|
|
88
|
+
[filter, options]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def check_condition(condition, type, **opts)
|
|
92
|
+
(type == :if) == case condition
|
|
93
|
+
when Proc
|
|
94
|
+
condition.call(opts[:query])
|
|
95
|
+
when Symbol
|
|
96
|
+
opts[:filter].send(condition)
|
|
97
|
+
else
|
|
98
|
+
condition
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -4,6 +4,11 @@ module Jsonapi
|
|
|
4
4
|
module QueryBuilder
|
|
5
5
|
module Mixins
|
|
6
6
|
module Include
|
|
7
|
+
# Eager loads the relationships that will be included in the response, based on the Json:Api
|
|
8
|
+
# include query parameter.
|
|
9
|
+
# @param [ActiveRecord::Relation] collection
|
|
10
|
+
# @param [Object] include_params Optional explicit include params
|
|
11
|
+
# @return [ActiveRecord::Relation] Collection with eager loaded included relations
|
|
7
12
|
def add_includes(collection, include_params = send(:include_params))
|
|
8
13
|
collection.includes(formatted_include_params(include_params))
|
|
9
14
|
end
|
|
@@ -4,14 +4,30 @@ module Jsonapi
|
|
|
4
4
|
module QueryBuilder
|
|
5
5
|
module Mixins
|
|
6
6
|
module Paginate
|
|
7
|
-
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
# Sets the paginator used to page the results. Defaults to Pagy
|
|
11
|
+
#
|
|
12
|
+
# @param [Jsonapi::QueryBuilder::Paginator::BasePaginator] paginator A subclass of BasePaginator
|
|
13
|
+
def paginator(paginator)
|
|
14
|
+
@paginator = paginator
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def _paginator
|
|
18
|
+
@paginator || Paginator::Pagy
|
|
19
|
+
end
|
|
20
|
+
end
|
|
8
21
|
|
|
9
22
|
attr_reader :pagination_details
|
|
10
23
|
|
|
24
|
+
# Paginates the collection and returns the requested page. Also sets the pagination details that can be used for
|
|
25
|
+
# displaying metadata in the Json:Api response.
|
|
26
|
+
# @param [ActiveRecord::Relation] collection
|
|
27
|
+
# @param [Object] page_params Optional explicit pagination params
|
|
28
|
+
# @return [ActiveRecord::Relation] Paged collection
|
|
11
29
|
def paginate(collection, page_params = send(:page_params))
|
|
12
|
-
@pagination_details
|
|
13
|
-
items: page_params[:size],
|
|
14
|
-
outset: page_params[:offset]
|
|
30
|
+
records, @pagination_details = self.class._paginator.new(collection).paginate(page_params)
|
|
15
31
|
|
|
16
32
|
records
|
|
17
33
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jsonapi
|
|
4
|
+
module QueryBuilder
|
|
5
|
+
module Mixins
|
|
6
|
+
module Sort
|
|
7
|
+
class Param
|
|
8
|
+
attr_reader :descending, :attribute
|
|
9
|
+
|
|
10
|
+
def initialize(param)
|
|
11
|
+
@descending, @attribute = deserialize(param)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def deserialize_params(sort_params)
|
|
16
|
+
(sort_params || "").split(",").map(&method(:new))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def deserialize(param)
|
|
21
|
+
_, descending, attribute = param.strip.match(/^(?<descending>-)?(?<attribute>.*)$/).to_a
|
|
22
|
+
|
|
23
|
+
[descending, attribute]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def serialize
|
|
27
|
+
[descending, attribute].compact.join
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def direction
|
|
31
|
+
descending.present? ? :desc : :asc
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "jsonapi/query_builder/mixins/sort/param"
|
|
4
|
+
require "jsonapi/query_builder/errors/unpermitted_sort_parameters"
|
|
5
|
+
|
|
3
6
|
module Jsonapi
|
|
4
7
|
module QueryBuilder
|
|
5
8
|
module Mixins
|
|
6
9
|
module Sort
|
|
7
10
|
extend ActiveSupport::Concern
|
|
8
11
|
|
|
9
|
-
UnpermittedSortParameters = Class.new ArgumentError
|
|
10
|
-
|
|
11
12
|
class_methods do
|
|
12
13
|
attr_reader :_default_sort
|
|
13
14
|
|
|
@@ -15,28 +16,57 @@ module Jsonapi
|
|
|
15
16
|
@_unique_sort_attributes || [id: :asc]
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
def
|
|
19
|
-
@
|
|
19
|
+
def supported_sorts
|
|
20
|
+
@supported_sorts || {}
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
# Ensures deterministic ordering. Defaults to :id in ascending direction.
|
|
24
|
+
# @param [Array<Symbol, String, Hash>] attributes An array of attributes or a hash with the attribute and it's
|
|
25
|
+
# order direction.
|
|
22
26
|
def unique_sort_attributes(*attributes)
|
|
23
27
|
@_unique_sort_attributes = attributes
|
|
24
28
|
end
|
|
29
|
+
|
|
25
30
|
alias_method :unique_sort_attribute, :unique_sort_attributes
|
|
26
31
|
|
|
32
|
+
# The :default_sort: can be set to sort by any field like `created_at` timestamp or similar. It is only used
|
|
33
|
+
# if no sort parameter is set, unlike the `unique_sort_attribute` which is always appended as the last sort
|
|
34
|
+
# attribute. The parameters are passed directly to the underlying active record relation, so the usual
|
|
35
|
+
# ordering options are possible.
|
|
36
|
+
# @param [Symbol, Hash] options A default sort attribute or a Hash with the attribute and it's order direction.
|
|
27
37
|
def default_sort(options)
|
|
28
38
|
@_default_sort = options
|
|
29
39
|
end
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
# Registers attribute that can be used for sorting. Sorting parameters are usually parsed from the `json:api`
|
|
42
|
+
# sort query parameter in the order they are given.
|
|
43
|
+
# @param [Symbol] attribute The "sortable" attribute
|
|
44
|
+
# @param [proc, Class] sort A proc or a sort class, defaults to a simple order(attribute => direction)
|
|
45
|
+
def sorts_by(attribute, sort = nil)
|
|
46
|
+
sort ||= ->(collection, direction) { collection.order(attribute => direction) }
|
|
47
|
+
@supported_sorts = {**supported_sorts, attribute => sort}
|
|
33
48
|
end
|
|
34
49
|
end
|
|
35
50
|
|
|
51
|
+
# Sorts the passed relation with the default sort params (parsed from the queries params) or with explicitly
|
|
52
|
+
# passed sort parameters.
|
|
53
|
+
# Parses each sort parameter and looks for the sorting strategy for it, if the strategy responds to a call
|
|
54
|
+
# method it calls it with the collection and parameter's parsed sort direction, otherwise it instantiates the
|
|
55
|
+
# sort class with the collection and the parameter's parsed sort direction and calls for the results. Finally it
|
|
56
|
+
# adds the unique sort attributes to enforce deterministic results. If sort params are blank, it adds the
|
|
57
|
+
# default sort attributes before setting the unique sort attributes.
|
|
58
|
+
# @param [ActiveRecord::Relation] collection
|
|
59
|
+
# @param [Object] sort_params Optional explicit sort params
|
|
60
|
+
# @return [ActiveRecord::Relation] Sorted relation
|
|
61
|
+
# @raise [Jsonapi::QueryBuilder::Errors::UnpermittedSortParameters] if not all sort parameters are
|
|
62
|
+
# permitted
|
|
36
63
|
def sort(collection, sort_params = send(:sort_params))
|
|
64
|
+
sort_params = Param.deserialize_params(sort_params)
|
|
65
|
+
ensure_permitted_sort_params!(sort_params) if sort_params
|
|
66
|
+
|
|
37
67
|
collection
|
|
38
|
-
.
|
|
39
|
-
.
|
|
68
|
+
.yield_self { |c| add_order_attributes(c, sort_params) }
|
|
69
|
+
.yield_self(&method(:add_unique_order_attributes))
|
|
40
70
|
end
|
|
41
71
|
|
|
42
72
|
private
|
|
@@ -45,27 +75,30 @@ module Jsonapi
|
|
|
45
75
|
params[:sort]
|
|
46
76
|
end
|
|
47
77
|
|
|
48
|
-
def
|
|
49
|
-
|
|
50
|
-
|
|
78
|
+
def ensure_permitted_sort_params!(sort_params)
|
|
79
|
+
unpermitted_parameters = sort_params.map(&:attribute).map(&:to_sym) - self.class.supported_sorts.keys
|
|
80
|
+
return if unpermitted_parameters.size.zero?
|
|
51
81
|
|
|
52
|
-
|
|
53
|
-
sort_params
|
|
54
|
-
.split(",")
|
|
55
|
-
.map(&:strip)
|
|
56
|
-
.to_h { |attribute| attribute.start_with?("-") ? [attribute[1..-1], :desc] : [attribute, :asc] }
|
|
57
|
-
.symbolize_keys
|
|
58
|
-
.tap(&method(:ensure_permitted_sort_params!))
|
|
82
|
+
raise Errors::UnpermittedSortParameters, unpermitted_parameters
|
|
59
83
|
end
|
|
60
84
|
|
|
61
|
-
def
|
|
62
|
-
return if
|
|
85
|
+
def add_order_attributes(collection, sort_params)
|
|
86
|
+
return collection if self.class._default_sort.nil? && sort_params.blank?
|
|
87
|
+
return collection.order(self.class._default_sort) if sort_params.blank?
|
|
88
|
+
|
|
89
|
+
sort_params.reduce(collection) do |sorted_collection, sort_param|
|
|
90
|
+
sort = self.class.supported_sorts.fetch(sort_param.attribute.to_sym)
|
|
63
91
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def add_unique_order_attributes(collection)
|
|
101
|
+
collection.order(*self.class._unique_sort_attributes)
|
|
69
102
|
end
|
|
70
103
|
end
|
|
71
104
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jsonapi
|
|
4
|
+
module QueryBuilder
|
|
5
|
+
module Paginator
|
|
6
|
+
class BasePaginator
|
|
7
|
+
attr_reader :collection
|
|
8
|
+
|
|
9
|
+
# @param [ActiveRecord::Relation] collection
|
|
10
|
+
def initialize(collection)
|
|
11
|
+
@collection = collection
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param [Hash] page_params
|
|
15
|
+
# @return [[ActiveRecord::Relation, Hash]] Records and pagination details
|
|
16
|
+
def paginate(page_params)
|
|
17
|
+
raise NotImplementedError, "#{self.class} should implement ##{__method__}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "kaminari"
|
|
4
|
+
|
|
5
|
+
module Jsonapi
|
|
6
|
+
module QueryBuilder
|
|
7
|
+
module Paginator
|
|
8
|
+
class Kaminari < BasePaginator
|
|
9
|
+
def paginate(page_params)
|
|
10
|
+
paged_collection = collection
|
|
11
|
+
.page(page_params[:number])
|
|
12
|
+
.per(page_params[:size])
|
|
13
|
+
.padding(page_params[:offset])
|
|
14
|
+
|
|
15
|
+
[paged_collection, pagination_details(paged_collection, page_params)]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def pagination_details(collection, page_params)
|
|
21
|
+
{
|
|
22
|
+
number: collection.current_page,
|
|
23
|
+
size: collection.limit_value,
|
|
24
|
+
offset: page_params[:offset],
|
|
25
|
+
total: collection.total_count,
|
|
26
|
+
total_pages: collection.total_pages,
|
|
27
|
+
next_page: collection.next_page,
|
|
28
|
+
prev_page: collection.prev_page
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
|
|
5
|
+
module Jsonapi
|
|
6
|
+
module QueryBuilder
|
|
7
|
+
module Paginator
|
|
8
|
+
class Keyset < BasePaginator
|
|
9
|
+
DEFAULT_DIRECTION = :after
|
|
10
|
+
DEFAULT_LIMIT = 25
|
|
11
|
+
|
|
12
|
+
def paginate(page_params)
|
|
13
|
+
page_params = extract_pagination_params(page_params)
|
|
14
|
+
records = apply_pagination(collection, page_params)
|
|
15
|
+
|
|
16
|
+
[records, page_params]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def extract_pagination_params(params)
|
|
22
|
+
{
|
|
23
|
+
column: params.fetch(:column, nil),
|
|
24
|
+
position: params.fetch(:position, nil),
|
|
25
|
+
direction: params.fetch(:direction, DEFAULT_DIRECTION),
|
|
26
|
+
limit: params.fetch(:limit, DEFAULT_LIMIT)
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def apply_pagination(collection, pagination_params)
|
|
31
|
+
column = pagination_params[:column]
|
|
32
|
+
position = pagination_params[:position]
|
|
33
|
+
direction = pagination_params[:direction]
|
|
34
|
+
limit = pagination_params[:limit]
|
|
35
|
+
|
|
36
|
+
return collection unless column
|
|
37
|
+
|
|
38
|
+
collection = apply_order(collection, column, direction)
|
|
39
|
+
collection = collection.limit(limit.to_i)
|
|
40
|
+
|
|
41
|
+
return collection unless position
|
|
42
|
+
|
|
43
|
+
apply_filter(collection, column, position, direction)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def apply_order(collection, column, direction)
|
|
47
|
+
if direction.to_sym == DEFAULT_DIRECTION
|
|
48
|
+
collection.reorder(collection.arel_table[column].asc)
|
|
49
|
+
else
|
|
50
|
+
collection.reorder(collection.arel_table[column].desc)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def apply_filter(collection, column, position, direction)
|
|
55
|
+
if direction.to_sym == DEFAULT_DIRECTION
|
|
56
|
+
collection.where(collection.arel_table[column].gt(position))
|
|
57
|
+
else
|
|
58
|
+
collection.where(collection.arel_table[column].lt(position))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pagy"
|
|
4
|
+
require "pagy/extras/items"
|
|
5
|
+
|
|
6
|
+
module Jsonapi
|
|
7
|
+
module QueryBuilder
|
|
8
|
+
module Paginator
|
|
9
|
+
class Pagy < BasePaginator
|
|
10
|
+
include ::Pagy::Backend
|
|
11
|
+
|
|
12
|
+
def paginate(page_params)
|
|
13
|
+
@params = {page: page_params}
|
|
14
|
+
|
|
15
|
+
pagination_details, records = pagy collection, page: page_params[:number],
|
|
16
|
+
items: page_params[:size],
|
|
17
|
+
outset: page_params[:offset]
|
|
18
|
+
[records, pagination_details]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :params
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -4,9 +4,8 @@ require "active_support/concern"
|
|
|
4
4
|
require "active_support/core_ext/array/conversions"
|
|
5
5
|
require "active_support/core_ext/hash/keys"
|
|
6
6
|
require "active_support/core_ext/string/inflections"
|
|
7
|
-
require "pagy"
|
|
8
|
-
require "pagy/extras/items"
|
|
9
7
|
|
|
10
8
|
require "jsonapi/query_builder/version"
|
|
11
9
|
require "jsonapi/query_builder/base_query"
|
|
12
10
|
require "jsonapi/query_builder/base_filter"
|
|
11
|
+
require "jsonapi/query_builder/base_sort"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jsonapi-query_builder
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jure Cindro
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2021-09-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -17,9 +17,6 @@ dependencies:
|
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
19
|
version: '5'
|
|
20
|
-
- - "<="
|
|
21
|
-
- !ruby/object:Gem::Version
|
|
22
|
-
version: '6.1'
|
|
23
20
|
type: :runtime
|
|
24
21
|
prerelease: false
|
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -27,9 +24,6 @@ dependencies:
|
|
|
27
24
|
- - ">="
|
|
28
25
|
- !ruby/object:Gem::Version
|
|
29
26
|
version: '5'
|
|
30
|
-
- - "<="
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '6.1'
|
|
33
27
|
- !ruby/object:Gem::Dependency
|
|
34
28
|
name: pagy
|
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -100,6 +94,20 @@ dependencies:
|
|
|
100
94
|
- - ">="
|
|
101
95
|
- !ruby/object:Gem::Version
|
|
102
96
|
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: standard
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
103
111
|
- !ruby/object:Gem::Dependency
|
|
104
112
|
name: rubocop-rspec
|
|
105
113
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -128,6 +136,62 @@ dependencies:
|
|
|
128
136
|
- - ">="
|
|
129
137
|
- !ruby/object:Gem::Version
|
|
130
138
|
version: '0'
|
|
139
|
+
- !ruby/object:Gem::Dependency
|
|
140
|
+
name: kaminari
|
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - "~>"
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '1.2'
|
|
146
|
+
type: :development
|
|
147
|
+
prerelease: false
|
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - "~>"
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '1.2'
|
|
153
|
+
- !ruby/object:Gem::Dependency
|
|
154
|
+
name: activerecord
|
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
|
156
|
+
requirements:
|
|
157
|
+
- - ">="
|
|
158
|
+
- !ruby/object:Gem::Version
|
|
159
|
+
version: '0'
|
|
160
|
+
type: :development
|
|
161
|
+
prerelease: false
|
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
163
|
+
requirements:
|
|
164
|
+
- - ">="
|
|
165
|
+
- !ruby/object:Gem::Version
|
|
166
|
+
version: '0'
|
|
167
|
+
- !ruby/object:Gem::Dependency
|
|
168
|
+
name: sqlite3
|
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
|
170
|
+
requirements:
|
|
171
|
+
- - ">="
|
|
172
|
+
- !ruby/object:Gem::Version
|
|
173
|
+
version: '0'
|
|
174
|
+
type: :development
|
|
175
|
+
prerelease: false
|
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
177
|
+
requirements:
|
|
178
|
+
- - ">="
|
|
179
|
+
- !ruby/object:Gem::Version
|
|
180
|
+
version: '0'
|
|
181
|
+
- !ruby/object:Gem::Dependency
|
|
182
|
+
name: pry
|
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
|
184
|
+
requirements:
|
|
185
|
+
- - ">="
|
|
186
|
+
- !ruby/object:Gem::Version
|
|
187
|
+
version: '0'
|
|
188
|
+
type: :development
|
|
189
|
+
prerelease: false
|
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
191
|
+
requirements:
|
|
192
|
+
- - ">="
|
|
193
|
+
- !ruby/object:Gem::Version
|
|
194
|
+
version: '0'
|
|
131
195
|
description: |
|
|
132
196
|
`Jsonapi::QueryBuilder` serves the purpose of adding the json api query related SQL conditions to the already scoped collection, usually used in controller index actions.
|
|
133
197
|
|
|
@@ -138,12 +202,15 @@ executables: []
|
|
|
138
202
|
extensions: []
|
|
139
203
|
extra_rdoc_files: []
|
|
140
204
|
files:
|
|
205
|
+
- ".github/workflows/lint.yml"
|
|
206
|
+
- ".github/workflows/spec.yml"
|
|
141
207
|
- ".gitignore"
|
|
142
208
|
- ".rspec"
|
|
143
209
|
- ".rubocop.yml"
|
|
144
210
|
- ".ruby-version"
|
|
145
211
|
- CHANGELOG.md
|
|
146
212
|
- Gemfile
|
|
213
|
+
- Gemfile.lock
|
|
147
214
|
- LICENSE.txt
|
|
148
215
|
- README.md
|
|
149
216
|
- Rakefile
|
|
@@ -154,10 +221,18 @@ files:
|
|
|
154
221
|
- lib/jsonapi/query_builder.rb
|
|
155
222
|
- lib/jsonapi/query_builder/base_filter.rb
|
|
156
223
|
- lib/jsonapi/query_builder/base_query.rb
|
|
157
|
-
- lib/jsonapi/query_builder/
|
|
224
|
+
- lib/jsonapi/query_builder/base_sort.rb
|
|
225
|
+
- lib/jsonapi/query_builder/errors/unpermitted_sort_parameters.rb
|
|
226
|
+
- lib/jsonapi/query_builder/mixins/filter.rb
|
|
158
227
|
- lib/jsonapi/query_builder/mixins/include.rb
|
|
159
228
|
- lib/jsonapi/query_builder/mixins/paginate.rb
|
|
160
229
|
- lib/jsonapi/query_builder/mixins/sort.rb
|
|
230
|
+
- lib/jsonapi/query_builder/mixins/sort/param.rb
|
|
231
|
+
- lib/jsonapi/query_builder/paginator.rb
|
|
232
|
+
- lib/jsonapi/query_builder/paginator/base_paginator.rb
|
|
233
|
+
- lib/jsonapi/query_builder/paginator/kaminari.rb
|
|
234
|
+
- lib/jsonapi/query_builder/paginator/keyset.rb
|
|
235
|
+
- lib/jsonapi/query_builder/paginator/pagy.rb
|
|
161
236
|
- lib/jsonapi/query_builder/version.rb
|
|
162
237
|
homepage: https://github.com/infinum/jsonapi-query_builder
|
|
163
238
|
licenses:
|
|
@@ -172,14 +247,14 @@ require_paths:
|
|
|
172
247
|
- lib
|
|
173
248
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
174
249
|
requirements:
|
|
175
|
-
- - "
|
|
250
|
+
- - ">="
|
|
176
251
|
- !ruby/object:Gem::Version
|
|
177
252
|
version: '2.5'
|
|
178
253
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
179
254
|
requirements:
|
|
180
|
-
- - "
|
|
255
|
+
- - ">="
|
|
181
256
|
- !ruby/object:Gem::Version
|
|
182
|
-
version:
|
|
257
|
+
version: '0'
|
|
183
258
|
requirements: []
|
|
184
259
|
rubygems_version: 3.0.3
|
|
185
260
|
signing_key:
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Jsonapi
|
|
4
|
-
module QueryBuilder
|
|
5
|
-
module Mixins
|
|
6
|
-
module Filtering
|
|
7
|
-
extend ActiveSupport::Concern
|
|
8
|
-
|
|
9
|
-
class_methods do
|
|
10
|
-
def supported_filters
|
|
11
|
-
@supported_filters || {}
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def filters_by(attribute, filter = nil, **options)
|
|
15
|
-
filter ||= ->(collection, query) { collection.where(attribute => query) }
|
|
16
|
-
@supported_filters = {**supported_filters, attribute => [filter, options]}
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def filter(collection, filter_params = send(:filter_params))
|
|
21
|
-
self.class.supported_filters.reduce(collection) do |filtered_collection, supported_filter|
|
|
22
|
-
filter, options = serialize_filter(supported_filter, collection: filtered_collection, params: filter_params)
|
|
23
|
-
|
|
24
|
-
next filtered_collection unless options[:conditions].all? { |type, condition|
|
|
25
|
-
check_condition(condition, type, filter: filter, query: options[:query])
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
filter.respond_to?(:call) ? filter.call(filtered_collection, options[:query]) : filter.results
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def filter_params
|
|
35
|
-
params[:filter] || {}
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def serialize_filter(supported_filter, collection:, params:)
|
|
39
|
-
attribute, (filter, options) = *supported_filter
|
|
40
|
-
|
|
41
|
-
options[:query_parameter] = options[:query_parameter]&.to_sym || attribute
|
|
42
|
-
options[:query] = params[options[:query_parameter]].presence
|
|
43
|
-
options[:conditions] = options.slice(:if, :unless).presence ||
|
|
44
|
-
{if: options[:query].present? || options[:allow_nil] && params.key?(options[:query_parameter])}
|
|
45
|
-
|
|
46
|
-
filter = filter.new(collection, options[:query]) unless filter.respond_to?(:call)
|
|
47
|
-
|
|
48
|
-
[filter, options]
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def check_condition(condition, type, **opts)
|
|
52
|
-
(type == :if) == case condition
|
|
53
|
-
when Proc
|
|
54
|
-
condition.call(opts[:query])
|
|
55
|
-
when Symbol
|
|
56
|
-
opts[:filter].send(condition)
|
|
57
|
-
else
|
|
58
|
-
condition
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|