jsonapi-query_builder 0.1.4.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +164 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/jsonapi-query_builder.gemspec +50 -0
- data/lefthook.yml +36 -0
- data/lib/jsonapi/query_builder.rb +12 -0
- data/lib/jsonapi/query_builder/base_filter.rb +18 -0
- data/lib/jsonapi/query_builder/base_query.rb +32 -0
- data/lib/jsonapi/query_builder/mixins/filtering.rb +64 -0
- data/lib/jsonapi/query_builder/mixins/include.rb +36 -0
- data/lib/jsonapi/query_builder/mixins/paginate.rb +27 -0
- data/lib/jsonapi/query_builder/mixins/sort.rb +73 -0
- data/lib/jsonapi/query_builder/version.rb +7 -0
- metadata +188 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: cfe8817a1796e39ffa45d7ca78ea92fb476b8b114e59f058bf94509996fd72c7
|
4
|
+
data.tar.gz: b321a790c6cb02112b90cb458e498705ed66227506fc76ae8790a04fdb603e54
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1e418da7f6940422f315202f10bc603a138144787b5e97168a6e40b85719e624d2e862fb460dc4eb754e8890ee55ec53ad2e66a75cc27b1427252f1c8e8fd502
|
7
|
+
data.tar.gz: c6acc4f51df8f314fd2bb650e54160c0c4b549059a16964f8814d103abbd4f0eae1f666f02a59564f9efaf7e960f7e08c21874769bdf0f33ccb86973788f2cff
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.6.6
|
data/CHANGELOG.md
ADDED
File without changes
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Jure Cindro
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
# Jsonapi::QueryBuilder
|
2
|
+
|
3
|
+
`Jsonapi::QueryBuilder` serves the purpose of adding the json api query related SQL conditions to the already scoped
|
4
|
+
collection, usually used in controller index actions.
|
5
|
+
|
6
|
+
With the query builder we can easily define logic for query filters, attributes by which we can sort, and delegate
|
7
|
+
pagination parameters to the underlying paginator. Included relationships are automatically included via the
|
8
|
+
`ActiveRecord::QueryMethods#includes`, to prevent N+1 query problems.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'jsonapi-query_builder'
|
16
|
+
```
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle install
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install jsonapi-query_builder
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class UserQuery < Jsonapi::QueryBuilder::BaseQuery
|
30
|
+
## sorting
|
31
|
+
default_sort created_at: :desc
|
32
|
+
sorts_by :first_name, :last_name, :email
|
33
|
+
|
34
|
+
## filtering
|
35
|
+
filters_by :first_name
|
36
|
+
filters_by :last_name
|
37
|
+
filters_by :email, ->(collection, query) { collection.where('email ilike ?', "%#{query}%") }
|
38
|
+
filters_by :type, TypeFilter
|
39
|
+
filters_by :mrn, MrnInMemoryFilter
|
40
|
+
end
|
41
|
+
|
42
|
+
class UsersController < ApplicationController
|
43
|
+
def index
|
44
|
+
user_query = UserQuery.new(User, params.to_unsafe_hash)
|
45
|
+
|
46
|
+
render json: user_query.results, status: :ok
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
The query class is initialized using a collection and query parameters. Since query parameters are referenced explicitly
|
52
|
+
we can pass them as an unsafe hash. `Jsonapi::QueryBuilder::BaseQuery` should not be responsible for scoping records based on
|
53
|
+
current user permissions, or for any other type of scoping. It's only responsibility is to support the `json:api`
|
54
|
+
querying. Use `pundit` or similar for policy scoping, custom query objects for other scoping, and then pass the scoped
|
55
|
+
collection to the `Jsonapi::QueryBuilder::BaseQuery` object.
|
56
|
+
|
57
|
+
### Sorting
|
58
|
+
#### Ensuring deterministic results
|
59
|
+
Sorting has a fallback to an unique attribute which defaults to the `id` attribute. This ensures deterministic paginated
|
60
|
+
collection responses. You can override the `unique_sort_attribute` in the query object.
|
61
|
+
```ruby
|
62
|
+
# set the unique sort attribute
|
63
|
+
unique_sort_attribute :email
|
64
|
+
# use compound unique sort attributes
|
65
|
+
unique_sort_attributes :created_at, :email
|
66
|
+
````
|
67
|
+
#### Default sort options
|
68
|
+
The `default_sort` can be set to sort by any field like `created_at` timestamp or similar. It is only used if no sort
|
69
|
+
parameter is set, unlike the `unique_sort_attribute` which is always appended as the last sort attribute. The parameters
|
70
|
+
are passed directly to the underlying active record relation, so the usual ordering options are possible.
|
71
|
+
```ruby
|
72
|
+
default_sort created_at: :desc
|
73
|
+
```
|
74
|
+
#### Enabling sorting for attributes
|
75
|
+
`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
|
+
```ruby
|
79
|
+
sorts_by :first_name, :email
|
80
|
+
```
|
81
|
+
|
82
|
+
### Filtering
|
83
|
+
|
84
|
+
#### Simple exact match filters
|
85
|
+
```ruby
|
86
|
+
filters_by :first_name
|
87
|
+
# => collection.where(first_name: params.dig(:filter, :first_name)) if params.dig(:filter, :first_name).present?
|
88
|
+
```
|
89
|
+
|
90
|
+
#### Lambda as a filter
|
91
|
+
```ruby
|
92
|
+
filters_by :email, ->(collection, query) { collection.where('email ilike ?', "%#{query}%") }
|
93
|
+
# => collection.where('email ilike ?', "%#{params.dig(:filter, :email)}%") if params.dig(:filter, :email).present?
|
94
|
+
```
|
95
|
+
|
96
|
+
#### Filter classes
|
97
|
+
But since we're devout followers of the SOLID principles, we can define a filter class that responds to
|
98
|
+
`#results` method, which returns the filtered collection results. Under the hood the filter class is initialized with
|
99
|
+
the current scope and the query parameter is passed to the call method. This is great if you're using query objects for
|
100
|
+
ActiveRecord scopes, you can easily use them to filter as well.
|
101
|
+
```ruby
|
102
|
+
filters_by :type, TypeFilter
|
103
|
+
```
|
104
|
+
The filter class could look something like
|
105
|
+
```ruby
|
106
|
+
class TypeFilter < Jsonapi::QueryBuilder::BaseFilter
|
107
|
+
def results
|
108
|
+
collection.where(type: query.split(','))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
```
|
112
|
+
Sometimes you need to perform in-memory filtering, for example when database attributes are encrypted. In that case,
|
113
|
+
those filters should be applied last, the order of definition in the query object matters.
|
114
|
+
```ruby
|
115
|
+
class MrnFilter < Jsonapi::QueryBuilder::BaseFilter
|
116
|
+
def results
|
117
|
+
collection.select { |record| /#{query}/.match?(record.mrn) }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
#### Additional Options
|
123
|
+
You can override the filter query parameter name by passing the `query_parameter` option.
|
124
|
+
```ruby
|
125
|
+
filters_by :first_name, query_parameter: 'name'
|
126
|
+
# => collection.where(first_name: params.dig(:filter, :name)) if params.dig(:filter, :name).present?
|
127
|
+
```
|
128
|
+
`allow_nil` option changes the filter conditional to allow explicit checks for an attribute null value.
|
129
|
+
```ruby
|
130
|
+
filters_by :first_name, allow_nil: true
|
131
|
+
# => collection.where(first_name: params.dig(:filter, :first_name)) if params[:filter]&.key?(:first_name)
|
132
|
+
```
|
133
|
+
The conditional when the filter is applied can also be defined explicitly. Note that these options override the
|
134
|
+
`allow_nil` option, as the condition if defined explicitly and you should handle `nil` explicitly as well.
|
135
|
+
```ruby
|
136
|
+
filters_by :first_name, if: ->(query) { query.length >= 2 }
|
137
|
+
# => collection.where(first_name: params.dig(:filter, :first_name)) if params.dig(:filter, :first_name) >= 2
|
138
|
+
filters_by :first_name, unless: ->(query) { query.length < 2 }
|
139
|
+
# => collection.where(first_name: params.dig(:filter, :first_name)) unless params.dig(:filter, :first_name) < 2
|
140
|
+
```
|
141
|
+
When you're using a filter class you can pass a symbol to the `:if` and `:unless` options which invokes the method on
|
142
|
+
the filter class.
|
143
|
+
```ruby
|
144
|
+
filters_by :type, TypeFilter, if: :correct_type?
|
145
|
+
# => type_filter = TypeFilter.new(collection, query); type_filter.results if type_filter.correct_type?
|
146
|
+
```
|
147
|
+
|
148
|
+
## Development
|
149
|
+
|
150
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can
|
151
|
+
also run `bin/console` for an interactive prompt that will allow you to experiment.
|
152
|
+
|
153
|
+
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 version,
|
155
|
+
push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
156
|
+
|
157
|
+
## Contributing
|
158
|
+
|
159
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/infinum/jsonapi-query_builder.
|
160
|
+
|
161
|
+
|
162
|
+
## License
|
163
|
+
|
164
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "jsonapi/query_builder"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("../lib", __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "jsonapi/query_builder/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "jsonapi-query_builder"
|
9
|
+
spec.version = Jsonapi::QueryBuilder::VERSION
|
10
|
+
spec.authors = ["Jure Cindro"]
|
11
|
+
spec.email = ["jure.cindro@infinum.co"]
|
12
|
+
|
13
|
+
spec.summary = "Support `json:api` querying with ease!"
|
14
|
+
spec.description = <<~MD
|
15
|
+
`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.
|
16
|
+
|
17
|
+
With the query builder we can easily define logic for query filters, attributes by which we can sort, and delegate pagination parameters to the underlying paginator. Included relationships are automatically included via the `ActiveRecord::QueryMethods#includes`, to prevent N+1 query problems.
|
18
|
+
MD
|
19
|
+
spec.homepage = "https://github.com/infinum/jsonapi-query_builder"
|
20
|
+
spec.license = "MIT"
|
21
|
+
|
22
|
+
if spec.respond_to?(:metadata)
|
23
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
24
|
+
spec.metadata["source_code_uri"] = "https://github.com/infinum/jsonapi-query_builder"
|
25
|
+
spec.metadata["changelog_uri"] = "https://github.com/infinum/jsonapi-query_builder/blob/master/CHANGELOG.md"
|
26
|
+
else
|
27
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
28
|
+
end
|
29
|
+
|
30
|
+
# Specify which files should be added to the gem when it is released.
|
31
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
32
|
+
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
33
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
34
|
+
end
|
35
|
+
spec.bindir = "exe"
|
36
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
37
|
+
spec.require_paths = ["lib"]
|
38
|
+
|
39
|
+
spec.required_ruby_version = "~> 2.5"
|
40
|
+
|
41
|
+
spec.add_runtime_dependency "activerecord", ">= 5", "<= 6.1"
|
42
|
+
spec.add_runtime_dependency "pagy", "~> 3.5"
|
43
|
+
|
44
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
45
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
46
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
47
|
+
spec.add_development_dependency "standardrb"
|
48
|
+
spec.add_development_dependency "rubocop-rspec"
|
49
|
+
spec.add_development_dependency "lefthook"
|
50
|
+
end
|
data/lefthook.yml
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
lint:
|
2
|
+
commands: &lint
|
3
|
+
lint-frozen-strings:
|
4
|
+
glob: "*.rb"
|
5
|
+
run: bundle exec rubocop {staged_files} --only Style/FrozenStringLiteralComment,Layout/EmptyLineAfterMagicComment --format quiet --auto-correct
|
6
|
+
|
7
|
+
rubocop:
|
8
|
+
commands: &rubocop
|
9
|
+
rubocop-rspec:
|
10
|
+
glob: "spec/*"
|
11
|
+
run: bundle exec rubocop {staged_files} --only RSpec --format quiet
|
12
|
+
|
13
|
+
pre-commit:
|
14
|
+
parallel: true
|
15
|
+
commands:
|
16
|
+
<<: *lint
|
17
|
+
<<: *rubocop
|
18
|
+
standardrb:
|
19
|
+
glob: "*.rb"
|
20
|
+
run: bundle exec standardrb {staged_files}
|
21
|
+
rspec:
|
22
|
+
glob: "*_spec.rb"
|
23
|
+
run: bundle exec rspec {staged_files} --format failures
|
24
|
+
|
25
|
+
post-checkout:
|
26
|
+
commands:
|
27
|
+
bundle-install:
|
28
|
+
run: bundle check || bundle install
|
29
|
+
|
30
|
+
pre-push:
|
31
|
+
parallel: true
|
32
|
+
commands:
|
33
|
+
bundler-audit:
|
34
|
+
run: bundle audit --update --quiet
|
35
|
+
rspec:
|
36
|
+
run: bundle exec rspec --format failures
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
require "active_support/core_ext/array/conversions"
|
5
|
+
require "active_support/core_ext/hash/keys"
|
6
|
+
require "active_support/core_ext/string/inflections"
|
7
|
+
require "pagy"
|
8
|
+
require "pagy/extras/items"
|
9
|
+
|
10
|
+
require "jsonapi/query_builder/version"
|
11
|
+
require "jsonapi/query_builder/base_query"
|
12
|
+
require "jsonapi/query_builder/base_filter"
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jsonapi
|
4
|
+
module QueryBuilder
|
5
|
+
class BaseFilter
|
6
|
+
attr_reader :collection, :query
|
7
|
+
|
8
|
+
def initialize(collection, query)
|
9
|
+
@collection = collection
|
10
|
+
@query = query
|
11
|
+
end
|
12
|
+
|
13
|
+
def results
|
14
|
+
raise NotImplementedError, "#{self.class} should implement #results"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "jsonapi/query_builder/mixins/filtering"
|
4
|
+
require "jsonapi/query_builder/mixins/include"
|
5
|
+
require "jsonapi/query_builder/mixins/paginate"
|
6
|
+
require "jsonapi/query_builder/mixins/sort"
|
7
|
+
|
8
|
+
module Jsonapi
|
9
|
+
module QueryBuilder
|
10
|
+
class BaseQuery
|
11
|
+
include Mixins::Filtering
|
12
|
+
include Mixins::Include
|
13
|
+
include Mixins::Paginate
|
14
|
+
include Mixins::Sort
|
15
|
+
|
16
|
+
attr_reader :collection, :params
|
17
|
+
|
18
|
+
def initialize(collection, params)
|
19
|
+
@collection = collection
|
20
|
+
@params = params.deep_symbolize_keys
|
21
|
+
end
|
22
|
+
|
23
|
+
def results
|
24
|
+
collection
|
25
|
+
.yield_self(&method(:sort))
|
26
|
+
.yield_self(&method(:add_includes))
|
27
|
+
.yield_self(&method(:filter))
|
28
|
+
.yield_self(&method(:paginate))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,64 @@
|
|
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
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jsonapi
|
4
|
+
module QueryBuilder
|
5
|
+
module Mixins
|
6
|
+
module Include
|
7
|
+
def add_includes(collection, include_params = send(:include_params))
|
8
|
+
collection.includes(formatted_include_params(include_params))
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def include_params
|
14
|
+
params[:include]
|
15
|
+
end
|
16
|
+
|
17
|
+
def formatted_include_params(include_params)
|
18
|
+
return [] unless include_params
|
19
|
+
|
20
|
+
include_params
|
21
|
+
.split(",")
|
22
|
+
.map(&:strip)
|
23
|
+
.map(&method(:formatted_includes_relationship))
|
24
|
+
end
|
25
|
+
|
26
|
+
def formatted_includes_relationship(relationship)
|
27
|
+
parent, children = relationship.split(".", 2)
|
28
|
+
|
29
|
+
return parent.to_sym unless children
|
30
|
+
|
31
|
+
{parent.to_sym => formatted_includes_relationship(children)}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jsonapi
|
4
|
+
module QueryBuilder
|
5
|
+
module Mixins
|
6
|
+
module Paginate
|
7
|
+
include Pagy::Backend
|
8
|
+
|
9
|
+
attr_reader :pagination_details
|
10
|
+
|
11
|
+
def paginate(collection, page_params = send(:page_params))
|
12
|
+
@pagination_details, records = pagy collection, page: page_params[:number],
|
13
|
+
items: page_params[:size],
|
14
|
+
outset: page_params[:offset]
|
15
|
+
|
16
|
+
records
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def page_params
|
22
|
+
{number: 1, **params.fetch(:page, {})}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jsonapi
|
4
|
+
module QueryBuilder
|
5
|
+
module Mixins
|
6
|
+
module Sort
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
UnpermittedSortParameters = Class.new ArgumentError
|
10
|
+
|
11
|
+
class_methods do
|
12
|
+
attr_reader :_default_sort
|
13
|
+
|
14
|
+
def _unique_sort_attributes
|
15
|
+
@_unique_sort_attributes || [id: :asc]
|
16
|
+
end
|
17
|
+
|
18
|
+
def _sort_attributes
|
19
|
+
@_sort_attributes || []
|
20
|
+
end
|
21
|
+
|
22
|
+
def unique_sort_attributes(*attributes)
|
23
|
+
@_unique_sort_attributes = attributes
|
24
|
+
end
|
25
|
+
alias_method :unique_sort_attribute, :unique_sort_attributes
|
26
|
+
|
27
|
+
def default_sort(options)
|
28
|
+
@_default_sort = options
|
29
|
+
end
|
30
|
+
|
31
|
+
def sorts_by(*attributes)
|
32
|
+
@_sort_attributes = _sort_attributes + attributes
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def sort(collection, sort_params = send(:sort_params))
|
37
|
+
collection
|
38
|
+
.reorder(sort_params.nil? ? self.class._default_sort : formatted_sort_params(sort_params))
|
39
|
+
.tap(&method(:add_unique_order_attributes))
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def sort_params
|
45
|
+
params[:sort]
|
46
|
+
end
|
47
|
+
|
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
|
+
def ensure_permitted_sort_params!(sort_params)
|
62
|
+
return if (unpermitted_parameters = sort_params.keys - self.class._sort_attributes.map(&:to_sym)).size.zero?
|
63
|
+
|
64
|
+
raise UnpermittedSortParameters, [
|
65
|
+
unpermitted_parameters.to_sentence,
|
66
|
+
unpermitted_parameters.count == 1 ? "is not a" : "are not",
|
67
|
+
"permitted sort attribute".pluralize(unpermitted_parameters.count)
|
68
|
+
].join(" ")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
metadata
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jsonapi-query_builder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.4.pre
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jure Cindro
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-08-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5'
|
20
|
+
- - "<="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '6.1'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '5'
|
30
|
+
- - "<="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '6.1'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: pagy
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '3.5'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.5'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: bundler
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2.0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rake
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '13.0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '13.0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rspec
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '3.0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '3.0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: standardrb
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: rubocop-rspec
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: lefthook
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
description: |
|
132
|
+
`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
|
+
|
134
|
+
With the query builder we can easily define logic for query filters, attributes by which we can sort, and delegate pagination parameters to the underlying paginator. Included relationships are automatically included via the `ActiveRecord::QueryMethods#includes`, to prevent N+1 query problems.
|
135
|
+
email:
|
136
|
+
- jure.cindro@infinum.co
|
137
|
+
executables: []
|
138
|
+
extensions: []
|
139
|
+
extra_rdoc_files: []
|
140
|
+
files:
|
141
|
+
- ".gitignore"
|
142
|
+
- ".rspec"
|
143
|
+
- ".rubocop.yml"
|
144
|
+
- ".ruby-version"
|
145
|
+
- CHANGELOG.md
|
146
|
+
- Gemfile
|
147
|
+
- LICENSE.txt
|
148
|
+
- README.md
|
149
|
+
- Rakefile
|
150
|
+
- bin/console
|
151
|
+
- bin/setup
|
152
|
+
- jsonapi-query_builder.gemspec
|
153
|
+
- lefthook.yml
|
154
|
+
- lib/jsonapi/query_builder.rb
|
155
|
+
- lib/jsonapi/query_builder/base_filter.rb
|
156
|
+
- lib/jsonapi/query_builder/base_query.rb
|
157
|
+
- lib/jsonapi/query_builder/mixins/filtering.rb
|
158
|
+
- lib/jsonapi/query_builder/mixins/include.rb
|
159
|
+
- lib/jsonapi/query_builder/mixins/paginate.rb
|
160
|
+
- lib/jsonapi/query_builder/mixins/sort.rb
|
161
|
+
- lib/jsonapi/query_builder/version.rb
|
162
|
+
homepage: https://github.com/infinum/jsonapi-query_builder
|
163
|
+
licenses:
|
164
|
+
- MIT
|
165
|
+
metadata:
|
166
|
+
homepage_uri: https://github.com/infinum/jsonapi-query_builder
|
167
|
+
source_code_uri: https://github.com/infinum/jsonapi-query_builder
|
168
|
+
changelog_uri: https://github.com/infinum/jsonapi-query_builder/blob/master/CHANGELOG.md
|
169
|
+
post_install_message:
|
170
|
+
rdoc_options: []
|
171
|
+
require_paths:
|
172
|
+
- lib
|
173
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
174
|
+
requirements:
|
175
|
+
- - "~>"
|
176
|
+
- !ruby/object:Gem::Version
|
177
|
+
version: '2.5'
|
178
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
179
|
+
requirements:
|
180
|
+
- - ">"
|
181
|
+
- !ruby/object:Gem::Version
|
182
|
+
version: 1.3.1
|
183
|
+
requirements: []
|
184
|
+
rubygems_version: 3.0.3
|
185
|
+
signing_key:
|
186
|
+
specification_version: 4
|
187
|
+
summary: Support `json:api` querying with ease!
|
188
|
+
test_files: []
|