class JsonapiCompliable::Query

@attr_reader [Hash] params hash of query parameters with symbolized keys @attr_reader [Resource] resource the corresponding Resource object

Attributes

params[R]

TODO: This class could use some refactoring love!

resource[R]

TODO: This class could use some refactoring love!

Public Class Methods

default_hash() click to toggle source

This is the structure of +Query#to_hash+ used elsewhere in the library @see to_hash @api private @return [Hash] the default hash

# File lib/jsonapi_compliable/query.rb, line 12
def self.default_hash
  {
    filter: {},
    sort: [],
    page: {},
    include: {},
    stats: {},
    fields: {},
    extra_fields: {}
  }
end
new(resource, params) click to toggle source
# File lib/jsonapi_compliable/query.rb, line 24
def initialize(resource, params)
  @resource = resource
  @params = params
  @params = @params.permit! if @params.respond_to?(:permit!)
end

Public Instance Methods

association_names() click to toggle source

All the keys of the include_hash

For example, let's say we had

{ posts: { comments: {} }

#association_names would return

[:posts, :comments]

@return [Array<Symbol>] all association names, recursive

# File lib/jsonapi_compliable/query.rb, line 75
def association_names
  @association_names ||= Util::Hash.keys(include_hash)
end
include_directive() click to toggle source

The relevant include directive @see jsonapi-rb.org @return [JSONAPI::IncludeDirective]

# File lib/jsonapi_compliable/query.rb, line 33
def include_directive
  @include_directive ||= JSONAPI::IncludeDirective.new(params[:include])
end
include_hash() click to toggle source

The include, directive, as a hash. For instance

{ posts: { comments: {} } }

This will only include relationships that are

  • Available on the Resource

  • Whitelisted (when specified)

So that users can't simply request your entire object graph.

@see Util::IncludeParams @return [Hash] the scrubbed include directive as a hash

# File lib/jsonapi_compliable/query.rb, line 50
def include_hash
  @include_hash ||= begin
    requested = include_directive.to_hash

    whitelist = nil
    if resource.context
      whitelist = resource.context.sideload_whitelist
      whitelist = whitelist[resource.context_namespace] if whitelist
    end

    whitelist ? Util::IncludeParams.scrub(requested, whitelist) : requested
  end
end
to_hash() click to toggle source

A flat hash of sanitized query parameters. All relationship names are top-level:

{
  posts: { filter, sort, ... }
  comments: { filter, sort, ... }
}

@example sorting

# GET /posts?sort=-title
{ posts: { sort: { title: :desc } } }

@example pagination

# GET /posts?page[number]=2&page[size]=10
{ posts: { page: { number: 2, size: 10 } }

@example filtering

# GET /posts?filter[title]=Foo
{ posts: { filter: { title: 'Foo' } }

@example include

# GET /posts?include=comments.author
{ posts: { include: { comments: { author: {} } } } }

@example stats

# GET /posts?stats[likes]=count,average
{ posts: { stats: [:count, :average] } }

@example fields

# GET /posts?fields=foo,bar
{ posts: { fields: [:foo, :bar] } }

@example extra fields

# GET /posts?fields=foo,bar
{ posts: { extra_fields: [:foo, :bar] } }

@example nested parameters

# GET /posts?include=comments&sort=comments.created_at&page[comments][size]=3
{
  posts: { ... },
  comments: { page: { size: 3 }, sort: { created_at: :asc } }

@see default_hash @see Base#query_hash @return [Hash] the normalized query hash

# File lib/jsonapi_compliable/query.rb, line 124
def to_hash
  hash = { resource.type => self.class.default_hash }

  association_names.each do |name|
    hash[name] = self.class.default_hash
  end

  fields = parse_fields({}, :fields)
  extra_fields = parse_fields({}, :extra_fields)
  hash.each_pair do |type, query_hash|
    hash[type][:fields] = fields
    hash[type][:extra_fields] = extra_fields
  end

  parse_filter(hash)
  parse_sort(hash)
  parse_pagination(hash)

  parse_include(hash, include_hash, resource.type)
  parse_stats(hash)

  hash
end
zero_results?() click to toggle source

Check if the user has requested 0 actual results They may have done this to get, say, the total count without the overhead of fetching actual records.

@example Total Count, 0 Results

# GET /posts?page[size]=0&stats[total]=count
# Response:
{
  data: [],
  meta: {
    stats: { total: { count: 100 } }
  }
}

@return [Boolean] were 0 results requested?

# File lib/jsonapi_compliable/query.rb, line 163
def zero_results?
  !@params[:page].nil? &&
    !@params[:page][:size].nil? &&
    @params[:page][:size].to_i == 0
end

Private Instance Methods

association?(name) click to toggle source
# File lib/jsonapi_compliable/query.rb, line 171
def association?(name)
  resource.association_names.include?(name)
end
parse_fields(hash, type) click to toggle source
# File lib/jsonapi_compliable/query.rb, line 201
def parse_fields(hash, type)
  field_params = Util::FieldParams.parse(params[type])
  hash[type] = field_params
end
parse_filter(hash) click to toggle source
# File lib/jsonapi_compliable/query.rb, line 206
def parse_filter(hash)
  if filter = params[:filter]
    filter.each_pair do |key, value|
      key = key.to_sym

      if association?(key)
        symbolized_hash = value.to_h.each_with_object({}) do |(k, v), hash|
          hash[k.to_sym] = v
        end
        hash[key][:filter].merge!(symbolized_hash)
      else
        hash[resource.type][:filter][key] = value
      end
    end
  end
end
parse_include(memo, incl_hash, namespace) click to toggle source
# File lib/jsonapi_compliable/query.rb, line 175
def parse_include(memo, incl_hash, namespace)
  memo[namespace] ||= self.class.default_hash
  memo[namespace].merge!(include: incl_hash)

  incl_hash.each_pair do |key, sub_hash|
    key = Util::Sideload.namespace(namespace, key)
    memo.merge!(parse_include(memo, sub_hash, key))
  end

  memo
end
parse_pagination(hash) click to toggle source
# File lib/jsonapi_compliable/query.rb, line 242
def parse_pagination(hash)
  if pagination = params[:page]
    pagination.each_pair do |key, value|
      key = key.to_sym

      if [:number, :size].include?(key)
        hash[resource.type][:page][key] = value.to_i
      else
        hash[key][:page] = { number: value[:number].to_i, size: value[:size].to_i }
      end
    end
  end
end
parse_sort(hash) click to toggle source
# File lib/jsonapi_compliable/query.rb, line 223
def parse_sort(hash)
  if sort = params[:sort]
    sorts = sort.split(',')
    sorts.each do |s|
      if s.include?('.')
        type, attr = s.split('.')
        if type.starts_with?('-')
          type = type.sub('-', '')
          attr = "-#{attr}"
        end

        hash[type.to_sym][:sort] << sort_attr(attr)
      else
        hash[resource.type][:sort] << sort_attr(s)
      end
    end
  end
end
parse_stats(hash) click to toggle source
# File lib/jsonapi_compliable/query.rb, line 187
def parse_stats(hash)
  if params[:stats]
    params[:stats].each_pair do |namespace, calculations|
      if namespace == resource.type || association?(namespace)
        calculations.each_pair do |name, calcs|
          hash[namespace][:stats][name] = calcs.split(',').map(&:to_sym)
        end
      else
        hash[resource.type][:stats][namespace] = calculations.split(',').map(&:to_sym)
      end
    end
  end
end
sort_attr(attr) click to toggle source
# File lib/jsonapi_compliable/query.rb, line 256
def sort_attr(attr)
  value = attr.starts_with?('-') ? :desc : :asc
  key   = attr.sub('-', '').to_sym

  { key => value }
end