Skip to content
Closed
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
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.6.9
33 changes: 26 additions & 7 deletions lib/jsonapi/filtering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ module Filtering
#
# @param requested_field [String] the field to parse
# @return [Array] with the fields and the predicate
def self.extract_attributes_and_predicates(requested_field)
def self.extract_attributes_and_predicates(requested_field, allowed_fields)
predicates = []
field_name = requested_field.to_s.dup

while Ransack::Predicate.detect_from_string(field_name).present? do
# break if we have an exect match with an allowed_fields
# we do not want to pick apart the string further
break if allowed_fields.include?(field_name)

predicate = Ransack::Predicate
.detect_and_strip_from_string!(field_name)
predicates << Ransack::Predicate.named(predicate)
end

[field_name.split(/_and_|_or_/), predicates.reverse]
end

Expand All @@ -32,9 +35,11 @@ def self.extract_attributes_and_predicates(requested_field)
# @param allowed_fields [Array] a list of allowed fields to be filtered
# @param options [Hash] extra flags to enable/disable features
# @return [ActiveRecord::Base] a collection of resources
def jsonapi_filter(resources, allowed_fields, options = {})
def jsonapi_filter(resources, allowed_fields, *allowed_scopes, options)
options = options || {}
allowed_scopes = (allowed_scopes || []).flatten.map(&:to_s)
allowed_fields = allowed_fields.map(&:to_s)
extracted_params = jsonapi_filter_params(allowed_fields)
extracted_params = jsonapi_filter_params(allowed_fields, allowed_scopes)
extracted_params[:sorts] = jsonapi_sort_params(allowed_fields, options)
resources = resources.ransack(extracted_params)
block_given? ? yield(resources) : resources
Expand All @@ -47,21 +52,35 @@ def jsonapi_filter(resources, allowed_fields, options = {})
#
# @param allowed_fields [Array] a list of allowed fields to be filtered
# @return [Hash] to be passed to [ActiveRecord::Base#order]
def jsonapi_filter_params(allowed_fields)
def jsonapi_filter_params(allowed_fields, allowed_scopes)
filtered = {}
requested = params[:filter] || {}
allowed_fields = allowed_fields.map(&:to_s)

requested.each_pair do |requested_field, to_filter|
field_names, predicates = JSONAPI::Filtering
.extract_attributes_and_predicates(requested_field)
.extract_attributes_and_predicates(requested_field, allowed_fields)

wants_array = predicates.any? && predicates.map(&:wants_array).any?

if to_filter.is_a?(String) && wants_array
to_filter = to_filter.split(',')
end

# filter by scopes expects an exact match
# with the `allowed_scopes`. Predicates can be a part of named scopes
# and should be handled first
# Make sure to move to the next after a match
# {"created_before"=>"2013-02-01"}
# {"created_before_gt"=>"2013-02-01"}
if allowed_scopes.include?(requested_field)
filtered[requested_field] = to_filter
next
end


# filter by attributes
# {"first_name_eq"=>"Beau"}
if predicates.any? && (field_names - allowed_fields).empty?
filtered[requested_field] = to_filter
end
Expand All @@ -88,7 +107,7 @@ def jsonapi_sort_params(allowed_fields, options = {})
end

field_names, predicates = JSONAPI::Filtering
.extract_attributes_and_predicates(requested_field)
.extract_attributes_and_predicates(requested_field, allowed_fields)

next unless (field_names - allowed_fields).empty?
next if !options[:sort_with_expressions] && predicates.any?
Expand Down
6 changes: 3 additions & 3 deletions lib/jsonapi/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def self.install!
# @return [NilClass]
def self.add_errors_renderer!
ActionController::Renderers.add(:jsonapi_errors) do |resource, options|
self.content_type ||= Mime[:jsonapi]
self.content_type = Mime[:jsonapi] if self.media_type.nil?

many = JSONAPI::Rails.is_collection?(resource, options[:is_collection])
resource = [resource] unless many
Expand All @@ -47,7 +47,7 @@ def self.add_errors_renderer!
) unless resource.is_a?(ActiveModel::Errors)

errors = []
model = resource.instance_variable_get('@base')
model = resource.instance_variable_get(:@base)

if respond_to?(:jsonapi_serializer_class, true)
model_serializer = jsonapi_serializer_class(model, false)
Expand Down Expand Up @@ -90,7 +90,7 @@ def self.add_errors_renderer!
# @return [NilClass]
def self.add_renderer!
ActionController::Renderers.add(:jsonapi) do |resource, options|
self.content_type ||= Mime[:jsonapi]
self.content_type = Mime[:jsonapi] if self.media_type.nil?

JSONAPI_METHODS_MAPPING.to_a[0..1].each do |opt, method_name|
next unless respond_to?(method_name, true)
Expand Down
17 changes: 15 additions & 2 deletions spec/dummy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
create_table :users, force: true do |t|
t.string :first_name
t.string :last_name
t.integer :notes_count, default: 0
t.timestamps
end

Expand All @@ -31,6 +32,11 @@

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

def self.ransackable_scopes(auth_object = nil)
%i(created_before)
end
end

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

jsonapi_filter(User.all, allowed_fields, options) do |filtered|
jsonapi_filter(User.all,
allowed_fields,
allowed_scopes,
options) do |filtered|
result = filtered.result

if params[:sort].to_s.include?('notes_quantity')
Expand Down
1 change: 1 addition & 0 deletions spec/errors_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

it do
expect(response).to have_http_status(:unprocessable_entity)
expect(response.media_type).to eq('application/vnd.api+json')
expect(response_json['errors'].size).to eq(1)
expect(response_json['errors'][0]['status']).to eq('422')
expect(response_json['errors'][0]['title'])
Expand Down
1 change: 1 addition & 0 deletions spec/fetching_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
it do
expect(response).to have_http_status(:ok)
expect(response_json['data'].size).to eq(users.size)
expect(response.media_type).to eq('application/vnd.api+json')

response_json['data'].each do |item|
user = users.detect { |u| u.id == item['id'].to_i }
Expand Down
68 changes: 65 additions & 3 deletions spec/filtering_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,31 @@
describe '#extract_attributes_and_predicate' do
context 'mixed attributes (and/or)' do
it 'extracts ANDs' do
attributes, predicates = JSONAPI::Filtering
.extract_attributes_and_predicates('attr1_and_attr2_eq')
attributes, predicates =
JSONAPI::Filtering.extract_attributes_and_predicates(
'attr1_and_attr2_eq',
['attr1', 'attr2']
)
expect(attributes).to eq(['attr1', 'attr2'])
expect(predicates.size).to eq(1)
expect(predicates[0].name).to eq('eq')
end
end

context 'handle attributes with underscore in name' do
it 'detect _' do
attributes, predicates = JSONAPI::Filtering
.extract_attributes_and_predicates('notes_count_eq', ['notes_count'])
expect(attributes).to eq(['notes_count'])
expect(predicates.size).to eq(1)
expect(predicates[0].name).to eq('eq')
end
end

context 'mixed predicates' do
it 'extracts in order' do
attributes, predicates = JSONAPI::Filtering
.extract_attributes_and_predicates('attr1_sum_eq')
.extract_attributes_and_predicates('attr1_sum_eq', ['attr1'])
expect(attributes).to eq(['attr1'])
expect(predicates.size).to eq(2)
expect(predicates[0].name).to eq('sum')
Expand Down Expand Up @@ -85,6 +98,32 @@
end
end

context 'returns users by counter_cache' do
let(:params) do
second_user.update(notes_count: 1)
{
filter: { notes_count_eq: 1 }
}
end

it 'ensures ransack scopes work properly' do
ransack = User.ransack(notes_count_eq: 1)
expected_sql = 'SELECT "users".* FROM "users" WHERE '\
'"users"."notes_count" = 1'
expect(ransack.result.to_sql).to eq(expected_sql)
end

it do
expect(first_user.notes_count).to eq(0)
expect(second_user.notes_count).to eq(1)
expect(third_user.notes_count).to eq(0)

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

context 'returns sorted users by notes' do
let(:params) do
{ sort: '-notes_created_at' }
Expand All @@ -96,6 +135,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

it '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

it '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