From f8f8e7ea54873c0bbbf86a7c06f98cca9b69f64c Mon Sep 17 00:00:00 2001 From: kalle saas Date: Wed, 10 Mar 2021 07:25:50 +0100 Subject: [PATCH 1/3] add failing specs to test for ransack scopes --- spec/dummy.rb | 8 +++++++- spec/filtering_spec.rb | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/spec/dummy.rb b/spec/dummy.rb index 6f0c434..8b1644e 100644 --- a/spec/dummy.rb +++ b/spec/dummy.rb @@ -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 @@ -83,7 +89,7 @@ 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 } diff --git a/spec/filtering_spec.rb b/spec/filtering_spec.rb index f3bd80a..a078de0 100644 --- a/spec/filtering_spec.rb +++ b/spec/filtering_spec.rb @@ -96,6 +96,29 @@ 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 + + fit 'ensures ransack scopes are working properly' do + ransack = User.ransack({ created_before: '2013-02-01' }) + expected_sql = 'SELECT "users".* FROM "users" WHERE '\ + '(created_at < \'2013-02-01\')' + expect(ransack.result.to_sql).to eq(expected_sql) + end + + fit 'should return only' 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 From e9dc2e4cbbcd7b1734117b32668197a680e17dd1 Mon Sep 17 00:00:00 2001 From: Martin Meyerhoff Date: Thu, 24 Jun 2021 17:41:46 +0200 Subject: [PATCH 2/3] Do not filter out scopes from Ransack queries Scopes are special in that they don't have a predicate. This removes the check for the predicate, and thus makes scopes as filtering options work with JSONAPI.rb. --- lib/jsonapi/filtering.rb | 2 +- spec/dummy.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jsonapi/filtering.rb b/lib/jsonapi/filtering.rb index 70d47db..d09a619 100644 --- a/lib/jsonapi/filtering.rb +++ b/lib/jsonapi/filtering.rb @@ -62,7 +62,7 @@ def jsonapi_filter_params(allowed_fields) to_filter = to_filter.split(',') end - if predicates.any? && (field_names - allowed_fields).empty? + if (field_names - allowed_fields).empty? filtered[requested_field] = to_filter end end diff --git a/spec/dummy.rb b/spec/dummy.rb index 8b1644e..c8d6680 100644 --- a/spec/dummy.rb +++ b/spec/dummy.rb @@ -32,7 +32,7 @@ class User < ActiveRecord::Base has_many :notes - scope :created_before, ->(date) { where("created_at < ?", date) } + scope :created_before, ->(date) { where('created_at < ?', date) } def self.ransackable_scopes(_auth_object = nil) [:created_before] From 8fea92c5b0017a6605b20839af32c3b5d95ec720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Wed, 4 Aug 2021 17:52:43 +0100 Subject: [PATCH 3/3] Use `options` to enable/flag the use of scopes. --- README.md | 31 +++++++++++++++++++++++++++++++ lib/jsonapi/filtering.rb | 15 ++++++++++++--- spec/dummy.rb | 5 ++++- spec/filtering_spec.rb | 9 +-------- 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8c95c08..06633cb 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/lib/jsonapi/filtering.rb b/lib/jsonapi/filtering.rb index d09a619..14c6c43 100644 --- a/lib/jsonapi/filtering.rb +++ b/lib/jsonapi/filtering.rb @@ -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 @@ -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 @@ -62,7 +64,14 @@ def jsonapi_filter_params(allowed_fields) to_filter = to_filter.split(',') end - if (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) + ) + + if allow && (field_names - allowed_fields).empty? filtered[requested_field] = to_filter end end diff --git a/spec/dummy.rb b/spec/dummy.rb index c8d6680..fb6c749 100644 --- a/spec/dummy.rb +++ b/spec/dummy.rb @@ -91,7 +91,10 @@ def index :first_name, :last_name, :created_at, :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 diff --git a/spec/filtering_spec.rb b/spec/filtering_spec.rb index a078de0..40939be 100644 --- a/spec/filtering_spec.rb +++ b/spec/filtering_spec.rb @@ -106,14 +106,7 @@ } end - fit 'ensures ransack scopes are working properly' do - ransack = User.ransack({ created_before: '2013-02-01' }) - expected_sql = 'SELECT "users".* FROM "users" WHERE '\ - '(created_at < \'2013-02-01\')' - expect(ransack.result.to_sql).to eq(expected_sql) - end - - fit 'should return only' do + 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)