jsonapi-query_builder 0.1.6.pre → 0.1.7
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/README.md +26 -11
- data/jsonapi-query_builder.gemspec +2 -1
- 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 +18 -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 +21 -11
- 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: e613355258e5be273292baacd4b8c8aecba03b29005212749868412857feb678
|
4
|
+
data.tar.gz: 46f0f548604944cb9572b90c44a9e3ca5f15e1f8a47985cb588135f8cade2794
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 58546d6561f997295a4505780acb1ad9ff210bd8f1c1390638283d9af62cb1f4d4e89b69e646a902851a1840590b49ce3480ef42f1fff6861a0b7d92f3f27e80
|
7
|
+
data.tar.gz: 851a87dab11ee9194318f4fc5ea437d842ae561b2d5e10448adf796050cc0abb866a73c16ddde0e4b5f3d9757b583498344137d11ecdeaa793dc101a285af5f7
|
data/README.md
CHANGED
@@ -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,14 +73,26 @@ 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
|
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) }
|
80
89
|
```
|
81
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.
|
95
|
+
|
82
96
|
### Filtering
|
83
97
|
|
84
98
|
#### Simple exact match filters
|
@@ -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
|
```
|
@@ -151,8 +166,8 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
151
166
|
also run `bin/console` for an interactive prompt that will allow you to experiment.
|
152
167
|
|
153
168
|
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).
|
169
|
+
version number in `version.rb`, and then run `LEFTHOOK=0 bundle exec rake release`, which will create a git tag for the
|
170
|
+
version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
156
171
|
|
157
172
|
## Contributing
|
158
173
|
|
@@ -38,13 +38,14 @@ Gem::Specification.new do |spec|
|
|
38
38
|
|
39
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
|
@@ -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,34 +8,48 @@ 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
|
30
35
|
|
36
|
+
# @param [integer, string] id
|
37
|
+
# @return [Object]
|
38
|
+
# @raise [ActiveRecord::RecordNotFound] if the record by the id is not found
|
31
39
|
def find(id)
|
32
40
|
find_by! id: id
|
33
41
|
end
|
34
42
|
|
43
|
+
# Finds the record by the id parameter the class is instantiated with
|
44
|
+
# @return (see #find)
|
45
|
+
# @raise (see #find)
|
35
46
|
def record
|
36
47
|
find_by! id: params[:id]
|
37
48
|
end
|
38
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
|
39
53
|
def find_by!(**kwargs)
|
40
54
|
add_includes(collection).find_by!(kwargs)
|
41
55
|
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.7
|
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-01-19 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
|
@@ -154,10 +162,12 @@ files:
|
|
154
162
|
- lib/jsonapi/query_builder.rb
|
155
163
|
- lib/jsonapi/query_builder/base_filter.rb
|
156
164
|
- lib/jsonapi/query_builder/base_query.rb
|
157
|
-
- lib/jsonapi/query_builder/
|
165
|
+
- lib/jsonapi/query_builder/base_sort.rb
|
166
|
+
- lib/jsonapi/query_builder/mixins/filter.rb
|
158
167
|
- lib/jsonapi/query_builder/mixins/include.rb
|
159
168
|
- lib/jsonapi/query_builder/mixins/paginate.rb
|
160
169
|
- lib/jsonapi/query_builder/mixins/sort.rb
|
170
|
+
- lib/jsonapi/query_builder/mixins/sort/param.rb
|
161
171
|
- lib/jsonapi/query_builder/version.rb
|
162
172
|
homepage: https://github.com/infinum/jsonapi-query_builder
|
163
173
|
licenses:
|
@@ -177,9 +187,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
177
187
|
version: '2.5'
|
178
188
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
179
189
|
requirements:
|
180
|
-
- - "
|
190
|
+
- - ">="
|
181
191
|
- !ruby/object:Gem::Version
|
182
|
-
version:
|
192
|
+
version: '0'
|
183
193
|
requirements: []
|
184
194
|
rubygems_version: 3.0.3
|
185
195
|
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
|