Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ransackable scopes #57

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,37 @@ $ curl -X GET \
&sort=-model_attr,relationship_attr
```

#### Using scopes

You can use scopes for filtering as well, to enable scopes, use the option
flags:

```ruby
options = { allowed_scopes: User.ransackable_scopes }
jsonapi_filter(User.all, [:created_before], options) do |filtered|
render jsonapi: result.group('id').to_a
end
```

Assuming your model `User` has the following scope defined:

```ruby
class User < ActiveRecord::Base
scope :created_before, ->(date) { where('created_at < ?', date) }


def self.ransackable_scopes(_auth_object = nil)
[:created_before]
end
end
```

This allows you to run queries like:

```bash
$ curl -X GET /api/resources?filter[created_before]='2021-01-29'
```

#### Sorting using expressions

You can use basic aggregations like `min`, `max`, `avg`, `sum` and `count`
Expand Down
15 changes: 12 additions & 3 deletions lib/jsonapi/filtering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def self.extract_attributes_and_predicates(requested_field)
# @return [ActiveRecord::Base] a collection of resources
def jsonapi_filter(resources, allowed_fields, options = {})
allowed_fields = allowed_fields.map(&:to_s)
extracted_params = jsonapi_filter_params(allowed_fields)
extracted_params = jsonapi_filter_params(allowed_fields, options)
extracted_params[:sorts] = jsonapi_sort_params(allowed_fields, options)
resources = resources.ransack(extracted_params)
block_given? ? yield(resources) : resources
Expand All @@ -46,11 +46,13 @@ def jsonapi_filter(resources, allowed_fields, options = {})
# See: https://github.com/activerecord-hackery/ransack#search-matchers
#
# @param allowed_fields [Array] a list of allowed fields to be filtered
# @param options [Hash] extra flags to enable/disable features
# @return [Hash] to be passed to [ActiveRecord::Base#order]
def jsonapi_filter_params(allowed_fields)
def jsonapi_filter_params(allowed_fields, options = {})
filtered = {}
requested = params[:filter] || {}
allowed_fields = allowed_fields.map(&:to_s)
scopes = Array(options[:allowed_scopes]).map(&:to_s)

requested.each_pair do |requested_field, to_filter|
field_names, predicates = JSONAPI::Filtering
Expand All @@ -62,7 +64,14 @@ def jsonapi_filter_params(allowed_fields)
to_filter = to_filter.split(',')
end

if predicates.any? && (field_names - allowed_fields).empty?
# Disable predicate validation for scopes if the scope name is allowed
allow = predicates.any? || (
predicates.none? &&
scopes.include?(requested_field) &&
allowed_fields.include?(requested_field)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, given that we handle scopes separately already, I don't really see why we should add the scopes to the allowed_fields array as well. Most scopes will have names that don't directly match the name of a field, so I think this would be a little confusing.

However, I might be missing something as well!

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most scopes will have names that don't directly match the name of a field, so I think this would be a little confusing.

Yup, I was thinking about it too and the only reason I'd leave it explicit, is because there might be scopes that can conflict with the field name. My concern is that we might accidentally enable a scope as filterable without the user being aware of it.

@fluxsaas would be nice to get your opinion on this too. 🙇

)

if allow && (field_names - allowed_fields).empty?
filtered[requested_field] = to_filter
end
end
Expand Down
13 changes: 11 additions & 2 deletions spec/dummy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@

class User < ActiveRecord::Base
has_many :notes

scope :created_before, ->(date) { where('created_at < ?', date) }

def self.ransackable_scopes(_auth_object = nil)
[:created_before]
end
end

class Note < ActiveRecord::Base
Expand Down Expand Up @@ -83,9 +89,12 @@ class UsersController < ActionController::Base
def index
allowed_fields = [
:first_name, :last_name, :created_at,
:notes_created_at, :notes_quantity
:notes_created_at, :notes_quantity, :created_before
]
options = { sort_with_expressions: true }
options = {
sort_with_expressions: true,
allowed_scopes: User.ransackable_scopes
}

jsonapi_filter(User.all, allowed_fields, options) do |filtered|
result = filtered.result
Expand Down
16 changes: 16 additions & 0 deletions spec/filtering_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,22 @@
expect(response_json['data'][0]).to have_id(second_user.id.to_s)
end
end

context 'returns users filtered by scope' do
let(:params) do
third_user.update(created_at: '2013-01-01')

{
filter: { created_before: '2013-02-01' }
}
end

it do
expect(response).to have_http_status(:ok)
expect(response_json['data'].size).to eq(1)
expect(response_json['data'][0]).to have_id(third_user.id.to_s)
end
end
end
end
end