jsonapi-query_builder 0.1.4.pre → 0.1.9
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 +11 -0
- data/Gemfile.lock +92 -0
- data/README.md +30 -12
- data/jsonapi-query_builder.gemspec +3 -2
- data/lefthook.yml +2 -2
- data/lib/jsonapi/query_builder.rb +1 -0
- data/lib/jsonapi/query_builder/base_filter.rb +3 -0
- data/lib/jsonapi/query_builder/base_query.rb +30 -4
- data/lib/jsonapi/query_builder/base_sort.rb +21 -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 +5 -0
- data/lib/jsonapi/query_builder/mixins/sort.rb +58 -20
- data/lib/jsonapi/query_builder/mixins/sort/param.rb +37 -0
- data/lib/jsonapi/query_builder/version.rb +1 -1
- metadata +25 -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: 8c919f735d39662624a6cbda5429e5b31941578628bfa6adadf26334a0b9a1f3
|
|
4
|
+
data.tar.gz: 3ff9cadb9e8b8cea55447e6b86c6c8f3e16953beda7edee3ff8aac7e230753b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51d7f79e29b103e6aeac59dee1283b09ae3a9b3dc035099e341a2f9891a41fad1d7e05357198b4b08846226ac0421d195b3d583d9936a326003cfab076645ded
|
|
7
|
+
data.tar.gz: 6f11c9b84669ace622fb6b8d5d8917abeb9ced15ad1dc68e988380e282a7be9aa9d3e73baa317a49212c161c9aa351461c251fd89fd882b5e9c05847626ad698
|
|
@@ -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,11 @@
|
|
|
1
|
+
# Change log
|
|
2
|
+
|
|
3
|
+
## 0.1.9 (2021-05-07)
|
|
4
|
+
|
|
5
|
+
- [#18](https://github.com/infinum/jsonapi-query_builder/pull/18): Remove Ruby `to` version.
|
|
6
|
+
- [#9](https://github.com/infinum/jsonapi-query_builder/pull/9): added github actions
|
|
7
|
+
|
|
8
|
+
## 0.1.8 (2021-01-25)
|
|
9
|
+
|
|
10
|
+
- [#8](https://github.com/infinum/jsonapi-query_builder/pull/8): add support for ruby 3.0
|
|
11
|
+
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
jsonapi-query_builder (0.1.9)
|
|
5
|
+
activerecord (>= 5)
|
|
6
|
+
pagy (~> 3.5)
|
|
7
|
+
|
|
8
|
+
GEM
|
|
9
|
+
remote: https://rubygems.org/
|
|
10
|
+
specs:
|
|
11
|
+
activemodel (6.1.3.1)
|
|
12
|
+
activesupport (= 6.1.3.1)
|
|
13
|
+
activerecord (6.1.3.1)
|
|
14
|
+
activemodel (= 6.1.3.1)
|
|
15
|
+
activesupport (= 6.1.3.1)
|
|
16
|
+
activesupport (6.1.3.1)
|
|
17
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
18
|
+
i18n (>= 1.6, < 2)
|
|
19
|
+
minitest (>= 5.1)
|
|
20
|
+
tzinfo (~> 2.0)
|
|
21
|
+
zeitwerk (~> 2.3)
|
|
22
|
+
ast (2.4.1)
|
|
23
|
+
concurrent-ruby (1.1.8)
|
|
24
|
+
diff-lcs (1.4.4)
|
|
25
|
+
i18n (1.8.10)
|
|
26
|
+
concurrent-ruby (~> 1.0)
|
|
27
|
+
lefthook (0.7.2)
|
|
28
|
+
minitest (5.14.4)
|
|
29
|
+
pagy (3.11.0)
|
|
30
|
+
parallel (1.20.1)
|
|
31
|
+
parser (3.0.0.0)
|
|
32
|
+
ast (~> 2.4.1)
|
|
33
|
+
rainbow (3.0.0)
|
|
34
|
+
rake (13.0.3)
|
|
35
|
+
regexp_parser (2.0.3)
|
|
36
|
+
rexml (3.2.5)
|
|
37
|
+
rspec (3.10.0)
|
|
38
|
+
rspec-core (~> 3.10.0)
|
|
39
|
+
rspec-expectations (~> 3.10.0)
|
|
40
|
+
rspec-mocks (~> 3.10.0)
|
|
41
|
+
rspec-core (3.10.1)
|
|
42
|
+
rspec-support (~> 3.10.0)
|
|
43
|
+
rspec-expectations (3.10.1)
|
|
44
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
45
|
+
rspec-support (~> 3.10.0)
|
|
46
|
+
rspec-mocks (3.10.1)
|
|
47
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
48
|
+
rspec-support (~> 3.10.0)
|
|
49
|
+
rspec-support (3.10.1)
|
|
50
|
+
rubocop (1.7.0)
|
|
51
|
+
parallel (~> 1.10)
|
|
52
|
+
parser (>= 2.7.1.5)
|
|
53
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
54
|
+
regexp_parser (>= 1.8, < 3.0)
|
|
55
|
+
rexml
|
|
56
|
+
rubocop-ast (>= 1.2.0, < 2.0)
|
|
57
|
+
ruby-progressbar (~> 1.7)
|
|
58
|
+
unicode-display_width (>= 1.4.0, < 2.0)
|
|
59
|
+
rubocop-ast (1.4.0)
|
|
60
|
+
parser (>= 2.7.1.5)
|
|
61
|
+
rubocop-performance (1.9.2)
|
|
62
|
+
rubocop (>= 0.90.0, < 2.0)
|
|
63
|
+
rubocop-ast (>= 0.4.0)
|
|
64
|
+
rubocop-rspec (2.1.0)
|
|
65
|
+
rubocop (~> 1.0)
|
|
66
|
+
rubocop-ast (>= 1.1.0)
|
|
67
|
+
ruby-progressbar (1.11.0)
|
|
68
|
+
standard (0.11.0)
|
|
69
|
+
rubocop (= 1.7.0)
|
|
70
|
+
rubocop-performance (= 1.9.2)
|
|
71
|
+
standardrb (1.0.0)
|
|
72
|
+
standard
|
|
73
|
+
tzinfo (2.0.4)
|
|
74
|
+
concurrent-ruby (~> 1.0)
|
|
75
|
+
unicode-display_width (1.7.0)
|
|
76
|
+
zeitwerk (2.4.2)
|
|
77
|
+
|
|
78
|
+
PLATFORMS
|
|
79
|
+
ruby
|
|
80
|
+
|
|
81
|
+
DEPENDENCIES
|
|
82
|
+
bundler (~> 2.0)
|
|
83
|
+
jsonapi-query_builder!
|
|
84
|
+
lefthook
|
|
85
|
+
rake (~> 13.0)
|
|
86
|
+
rspec (~> 3.0)
|
|
87
|
+
rubocop-rspec
|
|
88
|
+
standard
|
|
89
|
+
standardrb
|
|
90
|
+
|
|
91
|
+
BUNDLED WITH
|
|
92
|
+
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.
|
|
@@ -29,7 +29,9 @@ Or install it yourself as:
|
|
|
29
29
|
class UserQuery < Jsonapi::QueryBuilder::BaseQuery
|
|
30
30
|
## sorting
|
|
31
31
|
default_sort created_at: :desc
|
|
32
|
-
sorts_by :
|
|
32
|
+
sorts_by :last_name
|
|
33
|
+
sorts_by :first_name, ->(collection, direction) { collection.order(name: direction) }
|
|
34
|
+
sorts_by :email, EmailSort
|
|
33
35
|
|
|
34
36
|
## filtering
|
|
35
37
|
filters_by :first_name
|
|
@@ -71,13 +73,25 @@ are passed directly to the underlying active record relation, so the usual order
|
|
|
71
73
|
```ruby
|
|
72
74
|
default_sort created_at: :desc
|
|
73
75
|
```
|
|
74
|
-
#### Enabling sorting for attributes
|
|
76
|
+
#### Enabling simple sorting for attributes
|
|
75
77
|
`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 }`
|
|
78
|
+
`json:api` sort query parameter in the order they are given. So `sort=-first_name,email` would translate to
|
|
79
|
+
`{ first_name: :desc, email: :asc }`
|
|
78
80
|
```ruby
|
|
79
|
-
sorts_by :first_name
|
|
81
|
+
sorts_by :first_name
|
|
82
|
+
sorts_by :email
|
|
80
83
|
```
|
|
84
|
+
#### Sorting with lambdas
|
|
85
|
+
`sorts_by` also supports passing a lambda to implement a custom order or reorder function. The parameters passed to the
|
|
86
|
+
lamdba are collection and the direction of the order, which is either `:desc` or `:asc`.
|
|
87
|
+
```ruby
|
|
88
|
+
sorts_by :first_name, ->(collection, direction) { collection.order(name: direction) }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### Sorting with sort classes
|
|
92
|
+
But since we're devout followers of the SOLID principles, we can define a sort class that responds to
|
|
93
|
+
`#results` method, which returns the sorted collection. Under the hood the sort class is initialized with
|
|
94
|
+
the current scope and the direction parameter.
|
|
81
95
|
|
|
82
96
|
### Filtering
|
|
83
97
|
|
|
@@ -94,10 +108,11 @@ filters_by :email, ->(collection, query) { collection.where('email ilike ?', "%#
|
|
|
94
108
|
```
|
|
95
109
|
|
|
96
110
|
#### Filter classes
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
the current scope and the query parameter
|
|
100
|
-
ActiveRecord scopes, you can easily use them to filter as well.
|
|
111
|
+
We can define a filter class that responds to `#results` method, which returns the filtered collection results. Under
|
|
112
|
+
the hood the filter class is initialized with the current scope and the query parameter. However, if the object responds
|
|
113
|
+
to a `call` method it sends the current scope and the query parameter to that instead. This is great if you're using
|
|
114
|
+
query objects for ActiveRecord scopes, you can easily use them to filter with as well.
|
|
115
|
+
|
|
101
116
|
```ruby
|
|
102
117
|
filters_by :type, TypeFilter
|
|
103
118
|
```
|
|
@@ -150,9 +165,12 @@ filters_by :type, TypeFilter, if: :correct_type?
|
|
|
150
165
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can
|
|
151
166
|
also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
152
167
|
|
|
168
|
+
We're using `standardrb` and `lefthook`. You can install lefthook hooks via `lefthook install`. It will run linters and
|
|
169
|
+
standardrb checks before commits, and a bundle audit + whole spec suite before push.
|
|
170
|
+
|
|
153
171
|
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).
|
|
172
|
+
version number in `version.rb`, and then run `LEFTHOOK=0 bundle exec rake release`, which will create a git tag for the
|
|
173
|
+
version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
156
174
|
|
|
157
175
|
## Contributing
|
|
158
176
|
|
|
@@ -36,15 +36,16 @@ 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"
|
|
50
51
|
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,6 +1,6 @@
|
|
|
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"
|
|
@@ -8,25 +8,51 @@ require "jsonapi/query_builder/mixins/sort"
|
|
|
8
8
|
module Jsonapi
|
|
9
9
|
module QueryBuilder
|
|
10
10
|
class BaseQuery
|
|
11
|
-
include Mixins::
|
|
11
|
+
include Mixins::Filter
|
|
12
12
|
include Mixins::Include
|
|
13
13
|
include Mixins::Paginate
|
|
14
14
|
include Mixins::Sort
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
attr_accessor :collection, :params
|
|
17
17
|
|
|
18
|
+
# @param [ActiveRecord::Relation] collection
|
|
19
|
+
# @param [Hash] params Json:Api query parameters
|
|
18
20
|
def initialize(collection, params)
|
|
19
21
|
@collection = collection
|
|
20
22
|
@params = params.deep_symbolize_keys
|
|
21
23
|
end
|
|
22
24
|
|
|
25
|
+
# @return [ActiveRecord::Relation] A collection with eager loaded relationships based on include params, filtered,
|
|
26
|
+
# ordered and lastly, paginated.
|
|
27
|
+
# @note Pagination details are saved to an instance variable and can be accessed via the #pagination_details attribute reader
|
|
23
28
|
def results
|
|
24
29
|
collection
|
|
25
|
-
.yield_self(&method(:sort))
|
|
26
30
|
.yield_self(&method(:add_includes))
|
|
31
|
+
.yield_self(&method(:sort))
|
|
27
32
|
.yield_self(&method(:filter))
|
|
28
33
|
.yield_self(&method(:paginate))
|
|
29
34
|
end
|
|
35
|
+
|
|
36
|
+
# @param [integer, string] id
|
|
37
|
+
# @return [Object]
|
|
38
|
+
# @raise [ActiveRecord::RecordNotFound] if the record by the id is not found
|
|
39
|
+
def find(id)
|
|
40
|
+
find_by! id: id
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Finds the record by the id parameter the class is instantiated with
|
|
44
|
+
# @return (see #find)
|
|
45
|
+
# @raise (see #find)
|
|
46
|
+
def record
|
|
47
|
+
find_by! id: params[:id]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param [Hash] kwargs Attributes with required values
|
|
51
|
+
# @return (see #find)
|
|
52
|
+
# @raise [ActiveRecord::RecordNotFound] if the record by the arguments is not found
|
|
53
|
+
def find_by!(**kwargs)
|
|
54
|
+
add_includes(collection).find_by!(kwargs)
|
|
55
|
+
end
|
|
30
56
|
end
|
|
31
57
|
end
|
|
32
58
|
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,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
|
|
@@ -8,6 +8,11 @@ module Jsonapi
|
|
|
8
8
|
|
|
9
9
|
attr_reader :pagination_details
|
|
10
10
|
|
|
11
|
+
# Paginates the collection and returns the requested page. Also sets the pagination details that can be used for
|
|
12
|
+
# displaying metadata in the Json:Api response.
|
|
13
|
+
# @param [ActiveRecord::Relation] collection
|
|
14
|
+
# @param [Object] page_params Optional explicit pagination params
|
|
15
|
+
# @return [ActiveRecord::Relation] Paged collection
|
|
11
16
|
def paginate(collection, page_params = send(:page_params))
|
|
12
17
|
@pagination_details, records = pagy collection, page: page_params[:number],
|
|
13
18
|
items: page_params[:size],
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "jsonapi/query_builder/mixins/sort/param"
|
|
4
|
+
|
|
3
5
|
module Jsonapi
|
|
4
6
|
module QueryBuilder
|
|
5
7
|
module Mixins
|
|
@@ -15,28 +17,57 @@ module Jsonapi
|
|
|
15
17
|
@_unique_sort_attributes || [id: :asc]
|
|
16
18
|
end
|
|
17
19
|
|
|
18
|
-
def
|
|
19
|
-
@
|
|
20
|
+
def supported_sorts
|
|
21
|
+
@supported_sorts || {}
|
|
20
22
|
end
|
|
21
23
|
|
|
24
|
+
# Ensures deterministic ordering. Defaults to :id in ascending direction.
|
|
25
|
+
# @param [Array<Symbol, String, Hash>] attributes An array of attributes or a hash with the attribute and it's
|
|
26
|
+
# order direction.
|
|
22
27
|
def unique_sort_attributes(*attributes)
|
|
23
28
|
@_unique_sort_attributes = attributes
|
|
24
29
|
end
|
|
30
|
+
|
|
25
31
|
alias_method :unique_sort_attribute, :unique_sort_attributes
|
|
26
32
|
|
|
33
|
+
# The :default_sort: can be set to sort by any field like `created_at` timestamp or similar. It is only used
|
|
34
|
+
# if no sort parameter is set, unlike the `unique_sort_attribute` which is always appended as the last sort
|
|
35
|
+
# attribute. The parameters are passed directly to the underlying active record relation, so the usual
|
|
36
|
+
# ordering options are possible.
|
|
37
|
+
# @param [Symbol, Hash] options A default sort attribute or a Hash with the attribute and it's order direction.
|
|
27
38
|
def default_sort(options)
|
|
28
39
|
@_default_sort = options
|
|
29
40
|
end
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
# Registers attribute that can be used for sorting. Sorting parameters are usually parsed from the `json:api`
|
|
43
|
+
# sort query parameter in the order they are given.
|
|
44
|
+
# @param [Symbol] attribute The "sortable" attribute
|
|
45
|
+
# @param [proc, Class] sort A proc or a sort class, defaults to a simple order(attribute => direction)
|
|
46
|
+
def sorts_by(attribute, sort = nil)
|
|
47
|
+
sort ||= ->(collection, direction) { collection.order(attribute => direction) }
|
|
48
|
+
@supported_sorts = {**supported_sorts, attribute => sort}
|
|
33
49
|
end
|
|
34
50
|
end
|
|
35
51
|
|
|
52
|
+
# Sorts the passed relation with the default sort params (parsed from the queries params) or with explicitly
|
|
53
|
+
# passed sort parameters.
|
|
54
|
+
# Parses each sort parameter and looks for the sorting strategy for it, if the strategy responds to a call
|
|
55
|
+
# method it calls it with the collection and parameter's parsed sort direction, otherwise it instantiates the
|
|
56
|
+
# sort class with the collection and the parameter's parsed sort direction and calls for the results. Finally it
|
|
57
|
+
# adds the unique sort attributes to enforce deterministic results. If sort params are blank, it adds the
|
|
58
|
+
# default sort attributes before setting the unique sort attributes.
|
|
59
|
+
# @param [ActiveRecord::Relation] collection
|
|
60
|
+
# @param [Object] sort_params Optional explicit sort params
|
|
61
|
+
# @return [ActiveRecord::Relation] Sorted relation
|
|
62
|
+
# @raise [Jsonapi::QueryBuilder::Mixins::Sort::UnpermittedSortParameters] if not all sort parameters are
|
|
63
|
+
# permitted
|
|
36
64
|
def sort(collection, sort_params = send(:sort_params))
|
|
65
|
+
sort_params = Param.deserialize_params(sort_params)
|
|
66
|
+
ensure_permitted_sort_params!(sort_params) if sort_params
|
|
67
|
+
|
|
37
68
|
collection
|
|
38
|
-
.
|
|
39
|
-
.
|
|
69
|
+
.yield_self { |c| add_order_attributes(c, sort_params) }
|
|
70
|
+
.yield_self(&method(:add_unique_order_attributes))
|
|
40
71
|
end
|
|
41
72
|
|
|
42
73
|
private
|
|
@@ -45,21 +76,9 @@ module Jsonapi
|
|
|
45
76
|
params[:sort]
|
|
46
77
|
end
|
|
47
78
|
|
|
48
|
-
def add_unique_order_attributes(collection)
|
|
49
|
-
collection.order(*self.class._unique_sort_attributes)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def formatted_sort_params(sort_params)
|
|
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!))
|
|
59
|
-
end
|
|
60
|
-
|
|
61
79
|
def ensure_permitted_sort_params!(sort_params)
|
|
62
|
-
|
|
80
|
+
unpermitted_parameters = sort_params.map(&:attribute).map(&:to_sym) - self.class.supported_sorts.keys
|
|
81
|
+
return if unpermitted_parameters.size.zero?
|
|
63
82
|
|
|
64
83
|
raise UnpermittedSortParameters, [
|
|
65
84
|
unpermitted_parameters.to_sentence,
|
|
@@ -67,6 +86,25 @@ module Jsonapi
|
|
|
67
86
|
"permitted sort attribute".pluralize(unpermitted_parameters.count)
|
|
68
87
|
].join(" ")
|
|
69
88
|
end
|
|
89
|
+
|
|
90
|
+
def add_order_attributes(collection, sort_params)
|
|
91
|
+
return collection if self.class._default_sort.nil? && sort_params.blank?
|
|
92
|
+
return collection.order(self.class._default_sort) if sort_params.blank?
|
|
93
|
+
|
|
94
|
+
sort_params.reduce(collection) do |sorted_collection, sort_param|
|
|
95
|
+
sort = self.class.supported_sorts.fetch(sort_param.attribute.to_sym)
|
|
96
|
+
|
|
97
|
+
if sort.respond_to?(:call)
|
|
98
|
+
sort.call(sorted_collection, sort_param.direction)
|
|
99
|
+
else
|
|
100
|
+
sort.new(sorted_collection, sort_param.direction).results
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def add_unique_order_attributes(collection)
|
|
106
|
+
collection.order(*self.class._unique_sort_attributes)
|
|
107
|
+
end
|
|
70
108
|
end
|
|
71
109
|
end
|
|
72
110
|
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
|
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.1.
|
|
4
|
+
version: 0.1.9
|
|
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-05-07 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
|
|
@@ -138,12 +146,15 @@ executables: []
|
|
|
138
146
|
extensions: []
|
|
139
147
|
extra_rdoc_files: []
|
|
140
148
|
files:
|
|
149
|
+
- ".github/workflows/lint.yml"
|
|
150
|
+
- ".github/workflows/spec.yml"
|
|
141
151
|
- ".gitignore"
|
|
142
152
|
- ".rspec"
|
|
143
153
|
- ".rubocop.yml"
|
|
144
154
|
- ".ruby-version"
|
|
145
155
|
- CHANGELOG.md
|
|
146
156
|
- Gemfile
|
|
157
|
+
- Gemfile.lock
|
|
147
158
|
- LICENSE.txt
|
|
148
159
|
- README.md
|
|
149
160
|
- Rakefile
|
|
@@ -154,10 +165,12 @@ files:
|
|
|
154
165
|
- lib/jsonapi/query_builder.rb
|
|
155
166
|
- lib/jsonapi/query_builder/base_filter.rb
|
|
156
167
|
- lib/jsonapi/query_builder/base_query.rb
|
|
157
|
-
- lib/jsonapi/query_builder/
|
|
168
|
+
- lib/jsonapi/query_builder/base_sort.rb
|
|
169
|
+
- lib/jsonapi/query_builder/mixins/filter.rb
|
|
158
170
|
- lib/jsonapi/query_builder/mixins/include.rb
|
|
159
171
|
- lib/jsonapi/query_builder/mixins/paginate.rb
|
|
160
172
|
- lib/jsonapi/query_builder/mixins/sort.rb
|
|
173
|
+
- lib/jsonapi/query_builder/mixins/sort/param.rb
|
|
161
174
|
- lib/jsonapi/query_builder/version.rb
|
|
162
175
|
homepage: https://github.com/infinum/jsonapi-query_builder
|
|
163
176
|
licenses:
|
|
@@ -172,14 +185,14 @@ require_paths:
|
|
|
172
185
|
- lib
|
|
173
186
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
174
187
|
requirements:
|
|
175
|
-
- - "
|
|
188
|
+
- - ">="
|
|
176
189
|
- !ruby/object:Gem::Version
|
|
177
190
|
version: '2.5'
|
|
178
191
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
179
192
|
requirements:
|
|
180
|
-
- - "
|
|
193
|
+
- - ">="
|
|
181
194
|
- !ruby/object:Gem::Version
|
|
182
|
-
version:
|
|
195
|
+
version: '0'
|
|
183
196
|
requirements: []
|
|
184
197
|
rubygems_version: 3.0.3
|
|
185
198
|
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
|