Module: APIHelper::Filterable

Extended by:
ActiveSupport::Concern
Defined in:
lib/api_helper/filterable.rb

Overview

Filterable

A filterable resource API supports requests to filter resources in collection by their fields, using the filter query parameter.

For example, the following is a request for all products that has a particular color:

GET /products?filter[color]=red

With this approach, multiple filters can be applied to a single request:

GET /products?filter[color]=red&filter[status]=in-stock

Multiple filters are applied with the AND condition.

A list separated by commas (“,”) can be used to filter by field matching one of the values:

GET /products?filter[color]=red,blue,yellow

A few functions: not, greater_then, less_then, greater_then_or_equal, less_then_or_equal, between, like, contains, null and blank can be used to filter the data, for example:

GET /products?filter[color]=not(red)
GET /products?filter[price]=greater_then(1000)
GET /products?filter[price]=less_then_or_equal(2000)
GET /products?filter[price]=between(1000,2000)
GET /products?filter[name]=like(%lovely%)
GET /products?filter[name]=contains(%lovely%)
GET /products?filter[provider]=null()
GET /products?filter[provider]=blank()

Usage

Include this Concern in your Action Controller:

SamplesController < ApplicationController
  include APIHelper::Filterable
end

or in your Grape API class:

class SampleAPI < Grape::API
  helpers APIHelper::Filterable
end

then use the filter method in the controller like this:

@products = filter(Post, filterable_fields: [:name, :price, :color])

The filter method will return a scoped model collection, based directly from the requested URL parameters.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.filter_param_desc(for_field: nil) ⇒ Object

Returns a description of the ‘fields’ URL parameter



164
165
166
167
168
169
170
# File 'lib/api_helper/filterable.rb', line 164

def self.filter_param_desc(for_field: nil)
  if for_field.present?
    "Filter data base on the '#{for_field}' field."
  else
    "Filter the data."
  end
end

Instance Method Details

#filter(resource, filterable_fields: [], ignore_unknown_fields: true) ⇒ Object

Filter resources of a collection from the request parameter

Params:

resource

ActiveRecord::Relation resource collection to filter data from

filterable_fields

Array of Symbols fields that are allowed to be filtered, defaults to all



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/api_helper/filterable.rb', line 74

def filter(resource, filterable_fields: [], ignore_unknown_fields: true)
  # parse the request parameter
  if params[:filter].is_a?(Hash) ||
     defined?(Rails) && Rails.version.to_i >= 5 && params[:filter].is_a?(ActionController::Parameters)
    @filter = params[:filter]
    filterable_fields = filterable_fields.map(&:to_s)

    # deal with each condition
    @filter.each_pair do |field, condition|
      # bypass fields that aren't be abled to filter with
      next if filterable_fields.present? && !filterable_fields.include?(field)

      # escape string to prevent SQL injection
      field = resource.connection.quote_string(field)

      next if ignore_unknown_fields && resource.columns_hash[field].blank?
      field_type = resource.columns_hash[field] && resource.columns_hash[field].type || :unknown

      # if a function is used
      if func = condition.match(/(?<function>[^\(\)]+)\((?<param>.*)\)/)

        db_column_name = begin
          raise if ActiveRecord::Base.configurations[Rails.env]['adapter'] != 'mysql2'
          "`#{resource.table_name}`.`#{field}`"
        rescue
          "\"#{resource.table_name}\".\"#{field}\""
        end

        case func[:function]
        when 'not'
          values = func[:param].split(',')
          values.map!(&:to_bool) if field_type == :boolean
          resource = resource.where.not(field => values)

        when 'greater_then'
          resource = resource
                     .where("#{db_column_name} > ?",
                            func[:param])

        when 'less_then'
          resource = resource
                     .where("#{db_column_name} < ?",
                            func[:param])

        when 'greater_then_or_equal'
          resource = resource
                     .where("#{db_column_name} >= ?",
                            func[:param])

        when 'less_then_or_equal'
          resource = resource
                     .where("#{db_column_name} <= ?",
                            func[:param])

        when 'between'
          param = func[:param].split(',')
          resource = resource
                     .where("#{db_column_name} BETWEEN ? AND ?",
                            param.first, param.last)

        when 'like'
          resource = resource
                     .where("#{db_column_name} LIKE ?",
                            func[:param])

        when 'contains'
          resource = resource
                     .where("#{db_column_name} LIKE ?",
                            "%#{func[:param]}%")

        when 'null'
          resource = resource.where(field => nil)

        when 'blank'
          resource = resource.where(field => [nil, ''])
        end

      # if not function
      else
        values = condition.split(',')
        values.map!(&:to_bool) if field_type == :boolean
        resource = resource.where(field => values)
      end
    end
  end

  return resource
end