json_api_filter 0.1.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -1
- data/.travis.yml +3 -0
- data/Gemfile.lock +143 -17
- data/README.md +33 -8
- data/json_api_filter.gemspec +5 -1
- data/lib/json_api_filter.rb +29 -8
- data/lib/json_api_filter/auto_join.rb +18 -0
- data/lib/json_api_filter/dispatch.rb +112 -0
- data/lib/json_api_filter/field_filters/base.rb +18 -0
- data/lib/json_api_filter/field_filters/compare.rb +54 -0
- data/lib/json_api_filter/field_filters/matcher.rb +35 -0
- data/lib/json_api_filter/field_filters/pagination.rb +19 -0
- data/lib/json_api_filter/field_filters/searcher.rb +12 -0
- data/lib/json_api_filter/field_filters/sorter.rb +19 -0
- data/lib/json_api_filter/filter_attributes.rb +36 -0
- data/lib/json_api_filter/value_parser.rb +37 -0
- data/lib/json_api_filter/version.rb +1 -1
- metadata +70 -6
- data/lib/json_api_filter/filter_by.rb +0 -29
- data/lib/json_api_filter/filters.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a0e5c959706685cf6cc047d4824c7ed0e0c35778b6b952aa3b305e5e0eb40f13
|
4
|
+
data.tar.gz: 3a87503e2341ae745d0ec6b5154049c95b0a28b527aeb8903ed000e53685bf3b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fee62197e5d3814185e35f7935b76daba4c96dcabd2e655e250512f3267e8295512fffe16301df3b18b089c4eb5b5cbed1a120793dd60518929aa666ba16b5e2
|
7
|
+
data.tar.gz: c183c0a031bac479dfa66baf3cb90a9ef6735719c7dc61a12fcbda50412a0239e5dcd6ca4ebd3ddffae23588f9cd90793123a79cd2a44107723a9f869b5d4b2f
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,39 +1,161 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
json_api_filter (0.
|
4
|
+
json_api_filter (0.2)
|
5
5
|
activesupport (>= 3.0.0)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
-
|
10
|
+
actioncable (6.1.3.1)
|
11
|
+
actionpack (= 6.1.3.1)
|
12
|
+
activesupport (= 6.1.3.1)
|
13
|
+
nio4r (~> 2.0)
|
14
|
+
websocket-driver (>= 0.6.1)
|
15
|
+
actionmailbox (6.1.3.1)
|
16
|
+
actionpack (= 6.1.3.1)
|
17
|
+
activejob (= 6.1.3.1)
|
18
|
+
activerecord (= 6.1.3.1)
|
19
|
+
activestorage (= 6.1.3.1)
|
20
|
+
activesupport (= 6.1.3.1)
|
21
|
+
mail (>= 2.7.1)
|
22
|
+
actionmailer (6.1.3.1)
|
23
|
+
actionpack (= 6.1.3.1)
|
24
|
+
actionview (= 6.1.3.1)
|
25
|
+
activejob (= 6.1.3.1)
|
26
|
+
activesupport (= 6.1.3.1)
|
27
|
+
mail (~> 2.5, >= 2.5.4)
|
28
|
+
rails-dom-testing (~> 2.0)
|
29
|
+
actionpack (6.1.3.1)
|
30
|
+
actionview (= 6.1.3.1)
|
31
|
+
activesupport (= 6.1.3.1)
|
32
|
+
rack (~> 2.0, >= 2.0.9)
|
33
|
+
rack-test (>= 0.6.3)
|
34
|
+
rails-dom-testing (~> 2.0)
|
35
|
+
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
36
|
+
actiontext (6.1.3.1)
|
37
|
+
actionpack (= 6.1.3.1)
|
38
|
+
activerecord (= 6.1.3.1)
|
39
|
+
activestorage (= 6.1.3.1)
|
40
|
+
activesupport (= 6.1.3.1)
|
41
|
+
nokogiri (>= 1.8.5)
|
42
|
+
actionview (6.1.3.1)
|
43
|
+
activesupport (= 6.1.3.1)
|
44
|
+
builder (~> 3.1)
|
45
|
+
erubi (~> 1.4)
|
46
|
+
rails-dom-testing (~> 2.0)
|
47
|
+
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
48
|
+
activejob (6.1.3.1)
|
49
|
+
activesupport (= 6.1.3.1)
|
50
|
+
globalid (>= 0.3.6)
|
51
|
+
activemodel (6.1.3.1)
|
52
|
+
activesupport (= 6.1.3.1)
|
53
|
+
activerecord (6.1.3.1)
|
54
|
+
activemodel (= 6.1.3.1)
|
55
|
+
activesupport (= 6.1.3.1)
|
56
|
+
activestorage (6.1.3.1)
|
57
|
+
actionpack (= 6.1.3.1)
|
58
|
+
activejob (= 6.1.3.1)
|
59
|
+
activerecord (= 6.1.3.1)
|
60
|
+
activesupport (= 6.1.3.1)
|
61
|
+
marcel (~> 1.0.0)
|
62
|
+
mini_mime (~> 1.0.2)
|
63
|
+
activesupport (6.1.3.1)
|
11
64
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
12
65
|
i18n (>= 1.6, < 2)
|
13
66
|
minitest (>= 5.1)
|
14
67
|
tzinfo (~> 2.0)
|
15
68
|
zeitwerk (~> 2.3)
|
69
|
+
builder (3.2.4)
|
70
|
+
byebug (11.1.3)
|
16
71
|
concurrent-ruby (1.1.8)
|
17
|
-
|
18
|
-
|
72
|
+
crass (1.0.6)
|
73
|
+
diff-lcs (1.4.4)
|
74
|
+
erubi (1.10.0)
|
75
|
+
globalid (0.4.2)
|
76
|
+
activesupport (>= 4.2.0)
|
77
|
+
i18n (1.8.10)
|
19
78
|
concurrent-ruby (~> 1.0)
|
79
|
+
loofah (2.9.0)
|
80
|
+
crass (~> 1.0.2)
|
81
|
+
nokogiri (>= 1.5.9)
|
82
|
+
mail (2.7.1)
|
83
|
+
mini_mime (>= 0.1.1)
|
84
|
+
marcel (1.0.1)
|
85
|
+
method_source (1.0.0)
|
86
|
+
mini_mime (1.0.3)
|
87
|
+
mini_portile2 (2.5.0)
|
20
88
|
minitest (5.14.4)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
89
|
+
nio4r (2.5.7)
|
90
|
+
nokogiri (1.11.2)
|
91
|
+
mini_portile2 (~> 2.5.0)
|
92
|
+
racc (~> 1.4)
|
93
|
+
racc (1.5.2)
|
94
|
+
rack (2.2.3)
|
95
|
+
rack-test (1.1.0)
|
96
|
+
rack (>= 1.0, < 3)
|
97
|
+
rails (6.1.3.1)
|
98
|
+
actioncable (= 6.1.3.1)
|
99
|
+
actionmailbox (= 6.1.3.1)
|
100
|
+
actionmailer (= 6.1.3.1)
|
101
|
+
actionpack (= 6.1.3.1)
|
102
|
+
actiontext (= 6.1.3.1)
|
103
|
+
actionview (= 6.1.3.1)
|
104
|
+
activejob (= 6.1.3.1)
|
105
|
+
activemodel (= 6.1.3.1)
|
106
|
+
activerecord (= 6.1.3.1)
|
107
|
+
activestorage (= 6.1.3.1)
|
108
|
+
activesupport (= 6.1.3.1)
|
109
|
+
bundler (>= 1.15.0)
|
110
|
+
railties (= 6.1.3.1)
|
111
|
+
sprockets-rails (>= 2.0.0)
|
112
|
+
rails-dom-testing (2.0.3)
|
113
|
+
activesupport (>= 4.2.0)
|
114
|
+
nokogiri (>= 1.6)
|
115
|
+
rails-html-sanitizer (1.3.0)
|
116
|
+
loofah (~> 2.3)
|
117
|
+
railties (6.1.3.1)
|
118
|
+
actionpack (= 6.1.3.1)
|
119
|
+
activesupport (= 6.1.3.1)
|
120
|
+
method_source
|
121
|
+
rake (>= 0.8.7)
|
122
|
+
thor (~> 1.0)
|
123
|
+
rake (13.0.3)
|
124
|
+
rspec (3.10.0)
|
125
|
+
rspec-core (~> 3.10.0)
|
126
|
+
rspec-expectations (~> 3.10.0)
|
127
|
+
rspec-mocks (~> 3.10.0)
|
128
|
+
rspec-core (3.10.1)
|
129
|
+
rspec-support (~> 3.10.0)
|
130
|
+
rspec-expectations (3.10.1)
|
29
131
|
diff-lcs (>= 1.2.0, < 2.0)
|
30
|
-
rspec-support (~> 3.
|
31
|
-
rspec-mocks (3.
|
132
|
+
rspec-support (~> 3.10.0)
|
133
|
+
rspec-mocks (3.10.2)
|
32
134
|
diff-lcs (>= 1.2.0, < 2.0)
|
33
|
-
rspec-support (~> 3.
|
34
|
-
rspec-
|
135
|
+
rspec-support (~> 3.10.0)
|
136
|
+
rspec-rails (5.0.1)
|
137
|
+
actionpack (>= 5.2)
|
138
|
+
activesupport (>= 5.2)
|
139
|
+
railties (>= 5.2)
|
140
|
+
rspec-core (~> 3.10)
|
141
|
+
rspec-expectations (~> 3.10)
|
142
|
+
rspec-mocks (~> 3.10)
|
143
|
+
rspec-support (~> 3.10)
|
144
|
+
rspec-support (3.10.2)
|
145
|
+
sprockets (4.0.2)
|
146
|
+
concurrent-ruby (~> 1.0)
|
147
|
+
rack (> 1, < 3)
|
148
|
+
sprockets-rails (3.2.2)
|
149
|
+
actionpack (>= 4.0)
|
150
|
+
activesupport (>= 4.0)
|
151
|
+
sprockets (>= 3.0.0)
|
152
|
+
sqlite3 (1.4.2)
|
153
|
+
thor (1.1.0)
|
35
154
|
tzinfo (2.0.4)
|
36
155
|
concurrent-ruby (~> 1.0)
|
156
|
+
websocket-driver (0.7.3)
|
157
|
+
websocket-extensions (>= 0.1.0)
|
158
|
+
websocket-extensions (0.1.5)
|
37
159
|
zeitwerk (2.4.2)
|
38
160
|
|
39
161
|
PLATFORMS
|
@@ -41,9 +163,13 @@ PLATFORMS
|
|
41
163
|
|
42
164
|
DEPENDENCIES
|
43
165
|
bundler (~> 1.16)
|
166
|
+
byebug
|
44
167
|
json_api_filter!
|
168
|
+
rails
|
45
169
|
rake (~> 13.0)
|
46
|
-
rspec (
|
170
|
+
rspec (>= 3.0)
|
171
|
+
rspec-rails
|
172
|
+
sqlite3
|
47
173
|
|
48
174
|
BUNDLED WITH
|
49
175
|
1.17.2
|
data/README.md
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# JsonApiFilter
|
2
2
|
|
3
|
-
Filter for rails controller based on JsonAPI spec: `/books?filter[library_id]=1,2&filter[author_id]=12`
|
3
|
+
Filter for rails controller based on JsonAPI spec: `/books?filter[library_id]=1,2&filter[author_id][eq]=12&filter[created_at][gt]=2021-02-02`
|
4
4
|
|
5
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/f620f88131ea9d8ff650/maintainability)](https://codeclimate.com/github/Blaked84/json_api_filter/maintainability)
|
5
6
|
[![Gem Version](https://badge.fury.io/rb/json_api_filter.svg)](https://badge.fury.io/rb/json_api_filter)
|
7
|
+
[![Build Status](https://travis-ci.com/Blaked84/json_api_filter.svg?branch=master)](https://travis-ci.com/Blaked84/json_api_filter)
|
6
8
|
|
7
9
|
## Installation
|
8
10
|
|
@@ -13,27 +15,30 @@ gem 'json_api_filter'
|
|
13
15
|
```
|
14
16
|
|
15
17
|
And then execute:
|
16
|
-
|
17
|
-
|
18
|
+
```bash
|
19
|
+
$ bundle
|
20
|
+
```
|
18
21
|
|
19
22
|
Or install it yourself as:
|
20
|
-
|
21
|
-
|
23
|
+
```bash
|
24
|
+
$ gem install json_api_filter
|
25
|
+
```
|
22
26
|
|
23
27
|
## Usage
|
24
28
|
|
25
29
|
### Quick start
|
26
30
|
|
27
|
-
To filter this request `/books?filter[library_id]=1,2&filter[author_id]=12`
|
31
|
+
To filter this request `/books?filter[library_id]=1,2&filter[author_id]=12&search=Lord of the ring`
|
28
32
|
|
29
33
|
```ruby
|
30
34
|
class Book < ApplicationController
|
31
35
|
|
32
36
|
include JsonApiFilter
|
33
37
|
permitted_filters %i[library_id author_id]
|
38
|
+
permitted_searches :user_search
|
34
39
|
|
35
40
|
def index
|
36
|
-
@books = Book
|
41
|
+
@books = json_api_filter(Book, params)
|
37
42
|
end
|
38
43
|
|
39
44
|
end
|
@@ -41,7 +46,27 @@ end
|
|
41
46
|
```
|
42
47
|
|
43
48
|
- `permitted_filters` let you define allowed attributes to filter on (mandatory)
|
44
|
-
- `
|
49
|
+
- `permitted_searches` let you define the allowed search method defined in you model what will be called if you pass `search` params in your request (can be a pg_search scope)
|
50
|
+
- `json_api_filter(scope, params)` return an active record relation (`Book::` in this example)
|
51
|
+
|
52
|
+
## Migration from 0.1
|
53
|
+
0.2.x version is not compatible with 0.1
|
54
|
+
In your controller, you will have to replace all occurrences of `attr_filter` as bellow :
|
55
|
+
|
56
|
+
### Before
|
57
|
+
```ruby
|
58
|
+
def index
|
59
|
+
@books = Book.all.where(attr_filter(params))
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
### After
|
64
|
+
```ruby
|
65
|
+
def index
|
66
|
+
@books = json_api_filter(Book, params)
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
45
70
|
## Development
|
46
71
|
|
47
72
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/json_api_filter.gemspec
CHANGED
@@ -37,6 +37,10 @@ Gem::Specification.new do |spec|
|
|
37
37
|
|
38
38
|
spec.add_development_dependency "bundler", "~> 1.16"
|
39
39
|
spec.add_development_dependency "rake", "~> 13.0"
|
40
|
-
spec.add_development_dependency "rspec", "
|
40
|
+
spec.add_development_dependency "rspec", ">= 3.0"
|
41
|
+
spec.add_development_dependency "rspec-rails"
|
42
|
+
spec.add_development_dependency "sqlite3"
|
43
|
+
spec.add_development_dependency 'rails'
|
44
|
+
spec.add_development_dependency 'byebug'
|
41
45
|
spec.add_dependency "activesupport", ">= 3.0.0"
|
42
46
|
end
|
data/lib/json_api_filter.rb
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
require "json_api_filter/version"
|
2
|
-
require "json_api_filter/
|
3
|
-
require "json_api_filter/
|
2
|
+
require "json_api_filter/auto_join"
|
3
|
+
require "json_api_filter/dispatch"
|
4
|
+
require "json_api_filter/filter_attributes"
|
5
|
+
require "json_api_filter/value_parser"
|
6
|
+
require "json_api_filter/field_filters/base"
|
7
|
+
require "json_api_filter/field_filters/matcher"
|
8
|
+
require "json_api_filter/field_filters/compare"
|
9
|
+
require "json_api_filter/field_filters/searcher"
|
10
|
+
require "json_api_filter/field_filters/sorter"
|
11
|
+
require "json_api_filter/field_filters/pagination"
|
4
12
|
require "active_support/concern"
|
5
13
|
require "active_support/core_ext/object/blank"
|
6
14
|
|
@@ -13,16 +21,19 @@ module JsonApiFilter
|
|
13
21
|
|
14
22
|
extend ::ActiveSupport::Concern
|
15
23
|
included do
|
16
|
-
|
17
|
-
|
24
|
+
|
25
|
+
# @param [ActiveRecord::Base] scope
|
26
|
+
def json_api_filter(scope, query_params = params)
|
18
27
|
unless self.class.json_api_permitted_filters.present?
|
19
28
|
raise ::JsonApiFilter::MissingPermittedFilterError
|
20
29
|
end
|
21
30
|
|
22
|
-
::JsonApiFilter::
|
23
|
-
|
24
|
-
query_params
|
25
|
-
|
31
|
+
::JsonApiFilter::Dispatch.new(
|
32
|
+
scope,
|
33
|
+
query_params,
|
34
|
+
allowed_filters: self.class.json_api_permitted_filters,
|
35
|
+
allowed_searches: self.class.json_api_permitted_searches
|
36
|
+
).process
|
26
37
|
end
|
27
38
|
|
28
39
|
def self.permitted_filters(val)
|
@@ -34,5 +45,15 @@ module JsonApiFilter
|
|
34
45
|
def self.json_api_permitted_filters
|
35
46
|
[]
|
36
47
|
end
|
48
|
+
|
49
|
+
def self.permitted_searches(global, **columns)
|
50
|
+
define_singleton_method(:json_api_permitted_searches) do
|
51
|
+
{ global: global, columns: columns }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.json_api_permitted_searches
|
56
|
+
{}
|
57
|
+
end
|
37
58
|
end
|
38
59
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module JsonApiFilter
|
2
|
+
class AutoJoin
|
3
|
+
|
4
|
+
attr_reader :association_name, :scope
|
5
|
+
|
6
|
+
# @param [ActiveRecord::Base, ActiveRecord_Relation] scope
|
7
|
+
# @param [String, Symbol] association_name
|
8
|
+
def initialize(scope, association_name)
|
9
|
+
@association_name = association_name
|
10
|
+
@scope = scope
|
11
|
+
end
|
12
|
+
|
13
|
+
def predicate
|
14
|
+
scope.joins(association_name.to_sym)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module JsonApiFilter
|
2
|
+
class Dispatch
|
3
|
+
|
4
|
+
attr_reader :params, :scope, :allowed_filters, :allowed_searches
|
5
|
+
|
6
|
+
# @param [ActiveRecord::Base] scope
|
7
|
+
# @param [Hash, ActionController::Parameters] params
|
8
|
+
# @param [Array<Symbol>] allowed_filters
|
9
|
+
def initialize(scope, params, allowed_filters:, allowed_searches:)
|
10
|
+
@params = params
|
11
|
+
@scope = scope
|
12
|
+
@allowed_filters = allowed_filters
|
13
|
+
@allowed_searches = allowed_searches
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [ActiveRecord_Relation]
|
17
|
+
def process
|
18
|
+
@scope = [
|
19
|
+
scope.all,
|
20
|
+
sort_predicate,
|
21
|
+
filters_predicate,
|
22
|
+
search_predicate,
|
23
|
+
join_predicate,
|
24
|
+
].compact.reduce(&:merge)
|
25
|
+
return scope if params[:pagination].nil?
|
26
|
+
scope.merge(pagination_predicate)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# @return [ActiveRecord::Base, NilClass]
|
32
|
+
def filters_predicate
|
33
|
+
#TODO: split this method
|
34
|
+
parser_params.fetch('filter', {}).map do |key, value|
|
35
|
+
next unless filters.include?(key)
|
36
|
+
|
37
|
+
if nested_filters.include?(key.to_sym)
|
38
|
+
next ::JsonApiFilter::FieldFilters::Matcher.new(
|
39
|
+
scope,
|
40
|
+
{key => value},
|
41
|
+
association: true
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
if value.class != ActiveSupport::HashWithIndifferentAccess
|
46
|
+
next ::JsonApiFilter::FieldFilters::Matcher.new(scope, {key => value})
|
47
|
+
end
|
48
|
+
|
49
|
+
::JsonApiFilter::FieldFilters::Compare.new(
|
50
|
+
scope,
|
51
|
+
{key => value},
|
52
|
+
allowed_searches: allowed_searches
|
53
|
+
)
|
54
|
+
end.compact.map(&:predicate).reduce(&:merge)
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [ActiveRecord::Base, NilClass]
|
58
|
+
def sort_predicate
|
59
|
+
sort = parser_params[:sort]
|
60
|
+
return nil if sort.blank?
|
61
|
+
|
62
|
+
::JsonApiFilter::FieldFilters::Sorter.new(scope, sort).predicate
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [ActiveRecord::Base, NilClass]
|
66
|
+
def search_predicate
|
67
|
+
return nil if parser_params[:search].blank?
|
68
|
+
return nil if allowed_searches[:global].nil?
|
69
|
+
|
70
|
+
::JsonApiFilter::FieldFilters::Searcher.new(
|
71
|
+
scope,
|
72
|
+
{allowed_searches[:global] => parser_params[:search]}
|
73
|
+
).predicate
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [ActiveRecord::Base, NilClass]
|
77
|
+
def pagination_predicate
|
78
|
+
return nil if parser_params[:pagination].nil?
|
79
|
+
|
80
|
+
::JsonApiFilter::FieldFilters::Pagination.new(
|
81
|
+
scope,
|
82
|
+
parser_params[:pagination]
|
83
|
+
).predicate
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [ActiveRecord::Base, NilClass]
|
87
|
+
def join_predicate
|
88
|
+
parser_params.fetch('filter', {}).map do |key, value|
|
89
|
+
next unless nested_filters.include?(key.to_sym)
|
90
|
+
|
91
|
+
::JsonApiFilter::AutoJoin.new(scope, key)
|
92
|
+
end.compact.map(&:predicate).reduce(&:merge)
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Hash]
|
96
|
+
def parser_params
|
97
|
+
return params.to_unsafe_h if params.class == ActionController::Parameters
|
98
|
+
|
99
|
+
params
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [Hash]
|
103
|
+
def filters
|
104
|
+
FilterAttributes.new(allowed_filters, parser_params[:filter]).process
|
105
|
+
end
|
106
|
+
|
107
|
+
def nested_filters
|
108
|
+
FilterAttributes.new(allowed_filters, parser_params[:filter]).nested_allowed_filter
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module JsonApiFilter
|
2
|
+
module FieldFilters
|
3
|
+
class Base
|
4
|
+
|
5
|
+
attr_reader :scope, :values, :association
|
6
|
+
|
7
|
+
# @param [ActiveRecord::Base] scope
|
8
|
+
# @param [Hash] values
|
9
|
+
# @param [Boolean] association
|
10
|
+
def initialize(scope, values, association: false )
|
11
|
+
@scope = scope
|
12
|
+
@values = values
|
13
|
+
@association = association
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
|
2
|
+
module JsonApiFilter
|
3
|
+
module FieldFilters
|
4
|
+
class Compare < Base
|
5
|
+
|
6
|
+
attr_reader :allowed_searches
|
7
|
+
|
8
|
+
def initialize(scope, values, allowed_searches:)
|
9
|
+
super(scope, values)
|
10
|
+
@allowed_searches = allowed_searches
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [ActiveRecord_Relation]
|
14
|
+
def predicate
|
15
|
+
column = values.keys.first
|
16
|
+
filter = values.first[1]
|
17
|
+
filter.map do |key, value|
|
18
|
+
if !WHERE_METHODS[key.to_sym].nil?
|
19
|
+
compare(column, key, value)
|
20
|
+
elsif key == "search"
|
21
|
+
next unless allowed_searches[:columns].keys.include?(column.to_sym)
|
22
|
+
::JsonApiFilter::FieldFilters::Searcher.new(
|
23
|
+
scope,
|
24
|
+
{allowed_searches[:columns][column.to_sym] => value}
|
25
|
+
).predicate
|
26
|
+
else
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
end.compact.reduce(&:merge)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
WHERE_METHODS = {
|
35
|
+
eq: "=",
|
36
|
+
ne: "!=",
|
37
|
+
gt: ">",
|
38
|
+
ge: ">=",
|
39
|
+
lt: "<",
|
40
|
+
le: "<=",
|
41
|
+
}
|
42
|
+
|
43
|
+
def compare(column, method, value)
|
44
|
+
if WHERE_METHODS[method.to_sym] == "="
|
45
|
+
# prefer this method for eq as it implicitely transforms enum into their integer representation
|
46
|
+
scope.where(column => value)
|
47
|
+
else
|
48
|
+
scope.where("#{column} #{WHERE_METHODS[method.to_sym]} ?", value)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module JsonApiFilter
|
2
|
+
module FieldFilters
|
3
|
+
class Matcher < Base
|
4
|
+
|
5
|
+
# @return [ActiveRecord_Relation]
|
6
|
+
def predicate
|
7
|
+
|
8
|
+
values.map do |key, value|
|
9
|
+
scope.where(
|
10
|
+
key_translation(key) => ::JsonApiFilter::ValueParser.parse(value)
|
11
|
+
)
|
12
|
+
end.reduce(&:merge)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
#TODO: move to another class
|
18
|
+
def key_translation(key)
|
19
|
+
return key unless association
|
20
|
+
|
21
|
+
table_name(key)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [ActiveRecord::Base]
|
25
|
+
def model_klass
|
26
|
+
scope.try(:klass) || scope
|
27
|
+
end
|
28
|
+
|
29
|
+
def table_name(association_name)
|
30
|
+
model_klass.reflections[association_name.to_s].table_name
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module JsonApiFilter
|
2
|
+
module FieldFilters
|
3
|
+
class Pagination < Base
|
4
|
+
|
5
|
+
# @return [ActiveRecord_Relation]
|
6
|
+
def predicate
|
7
|
+
page = values["page"]
|
8
|
+
per_page = values["perPage"]
|
9
|
+
result = scope
|
10
|
+
unless page.nil? || per_page.nil? || per_page == "-1"
|
11
|
+
result = result.limit(per_page.to_i)
|
12
|
+
result = result.offset((page.to_i - 1) * per_page.to_i)
|
13
|
+
end
|
14
|
+
result
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module JsonApiFilter
|
2
|
+
module FieldFilters
|
3
|
+
class Sorter < Base
|
4
|
+
|
5
|
+
# @return [ActiveRecord_Relation]
|
6
|
+
def predicate
|
7
|
+
return nil if values["by"].nil?
|
8
|
+
scope.order(values["by"] => order)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def order
|
14
|
+
ActiveModel::Type::Boolean.new.cast(values["desc"]) ? :desc : :asc
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module JsonApiFilter
|
2
|
+
class FilterAttributes
|
3
|
+
attr_reader :allowed_filters, :params
|
4
|
+
|
5
|
+
|
6
|
+
# @param [ActiveSupport::HashWithIndifferentAccess, Hash] allowed_filters
|
7
|
+
# @param [ActiveSupport::HashWithIndifferentAccess, Hash] params
|
8
|
+
def initialize(allowed_filters, params)
|
9
|
+
@allowed_filters = allowed_filters
|
10
|
+
@params = params
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [ActiveSupport::HashWithIndifferentAccess, Hash]
|
14
|
+
def process
|
15
|
+
params.select do |k,v|
|
16
|
+
allowed_filters.include?(k.to_sym) ||
|
17
|
+
allowed_filters.include?(k.to_s) ||
|
18
|
+
nested_allowed_filter.include?(k.to_sym) ||
|
19
|
+
nested_allowed_filter.include?(k.to_s)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Keys of nested allowed filters
|
24
|
+
# only one level of nesting supported
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# for: permitted_filters [:id, :author, :name, users: [:id]]
|
28
|
+
# it returns: :users
|
29
|
+
#
|
30
|
+
# @return [Array<Symbol>]
|
31
|
+
def nested_allowed_filter
|
32
|
+
allowed_filters.select{|f| f.class == Hash}.map(&:keys).flatten
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module JsonApiFilter
|
2
|
+
class ValueParser
|
3
|
+
|
4
|
+
attr_reader :value
|
5
|
+
|
6
|
+
# @param [String, Hash, HashWithIndifferentAccess] value
|
7
|
+
def initialize(value)
|
8
|
+
@value = value
|
9
|
+
end
|
10
|
+
|
11
|
+
# Transform string of coma separated values to Array
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# "1,2,3" #=> [1,2,3]
|
15
|
+
#
|
16
|
+
# @param [String, Hash, HashWithIndifferentAccess] value
|
17
|
+
# @return [Array]
|
18
|
+
def self.parse(value)
|
19
|
+
new(value).indifferent_parse
|
20
|
+
end
|
21
|
+
|
22
|
+
def indifferent_parse
|
23
|
+
return parse if value.is_a? String
|
24
|
+
parse_hash if value.is_a? HashWithIndifferentAccess
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse
|
28
|
+
return [value] unless value.include?(",")
|
29
|
+
value.split(',')
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse_hash
|
33
|
+
value.map{|k,v| {k => self.class.parse(v)}}.reduce(&:merge)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: json_api_filter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: '0.2'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Blaked
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-03
|
11
|
+
date: 2021-05-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -42,16 +42,72 @@ dependencies:
|
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '3.0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec-rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rails
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: byebug
|
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'
|
55
111
|
- !ruby/object:Gem::Dependency
|
56
112
|
name: activesupport
|
57
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -86,8 +142,16 @@ files:
|
|
86
142
|
- bin/setup
|
87
143
|
- json_api_filter.gemspec
|
88
144
|
- lib/json_api_filter.rb
|
89
|
-
- lib/json_api_filter/
|
90
|
-
- lib/json_api_filter/
|
145
|
+
- lib/json_api_filter/auto_join.rb
|
146
|
+
- lib/json_api_filter/dispatch.rb
|
147
|
+
- lib/json_api_filter/field_filters/base.rb
|
148
|
+
- lib/json_api_filter/field_filters/compare.rb
|
149
|
+
- lib/json_api_filter/field_filters/matcher.rb
|
150
|
+
- lib/json_api_filter/field_filters/pagination.rb
|
151
|
+
- lib/json_api_filter/field_filters/searcher.rb
|
152
|
+
- lib/json_api_filter/field_filters/sorter.rb
|
153
|
+
- lib/json_api_filter/filter_attributes.rb
|
154
|
+
- lib/json_api_filter/value_parser.rb
|
91
155
|
- lib/json_api_filter/version.rb
|
92
156
|
homepage: https://github.com/Blaked84/json_api_filter
|
93
157
|
licenses:
|
@@ -1,29 +0,0 @@
|
|
1
|
-
module JsonApiFilter
|
2
|
-
class FilterBy
|
3
|
-
|
4
|
-
attr_reader :attr, :value
|
5
|
-
|
6
|
-
def initialize(attr, value)
|
7
|
-
@attr = attr
|
8
|
-
@value = value
|
9
|
-
end
|
10
|
-
|
11
|
-
def to_hash
|
12
|
-
return {} unless parsed_value.present?
|
13
|
-
|
14
|
-
{ key.to_sym => parsed_value }
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def key
|
20
|
-
attr.to_sym
|
21
|
-
end
|
22
|
-
|
23
|
-
def parsed_value
|
24
|
-
value.split(',')
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
|
@@ -1,24 +0,0 @@
|
|
1
|
-
module JsonApiFilter
|
2
|
-
class Filters
|
3
|
-
attr_reader :allowed_filters, :params
|
4
|
-
|
5
|
-
def initialize(allowed_filters, params)
|
6
|
-
@allowed_filters = allowed_filters
|
7
|
-
@params = params
|
8
|
-
end
|
9
|
-
|
10
|
-
def to_hash
|
11
|
-
filters.inject(:merge)
|
12
|
-
end
|
13
|
-
|
14
|
-
private
|
15
|
-
|
16
|
-
def filters
|
17
|
-
allowed_filters.map do |attr|
|
18
|
-
value = params.fetch(:filter, {}).fetch(attr, "")
|
19
|
-
::JsonApiFilter::FilterBy.new(attr, value).to_hash
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|