class JsonapiCompliable::Scope

A Scope wraps an underlying object. It modifies that object using the corresponding Resource and Query, and how to resolve that underlying object scope.

@example Basic Controller usage

def index
  base  = Post.all
  scope = jsonapi_scope(base)
  scope.object == base # => true
  scope.object = scope.object.where(active: true)

  # actually fires sql
  scope.resolve #=> [#<Post ...>, #<Post ...>, etc]
end

Attributes

object[R]
unpaginated_object[R]

Public Class Methods

new(object, resource, query, opts = {}) click to toggle source

@param object - The underlying, chainable base scope object @param resource - The Resource that will process the object @param query - The Query object for the current request @param [Hash] opts Options to configure the Scope @option opts [String] :namespace The nested relationship name @option opts [Boolean] :filter Opt-out of filter scoping @option opts [Boolean] :extra_fields Opt-out of extra_fields scoping @option opts [Boolean] :sort Opt-out of sort scoping @option opts [Boolean] :pagination Opt-out of pagination scoping @option opts [Boolean] :default_paginate Opt-out of default pagination when not specified in request

# File lib/jsonapi_compliable/scope.rb, line 29
def initialize(object, resource, query, opts = {})
  @object    = object
  @resource  = resource
  @query     = query

  # Namespace for the 'outer' or 'main' resource is its type
  # For its relationships, its the relationship name
  # IOW when hitting /states, it's resource type 'states'
  # when hitting /authors?include=state its 'state'
  @namespace = opts.delete(:namespace) || resource.type

  apply_scoping(opts)
end

Public Instance Methods

query_hash() click to toggle source

The slice of Query#to_hash for the current namespace @see Query#to_hash @api private

# File lib/jsonapi_compliable/scope.rb, line 76
def query_hash
  @query_hash ||= @query.to_hash[@namespace]
end
resolve() { |resolved| ... } click to toggle source

Resolve the scope. This is where SQL is actually fired, an HTTP request is actually made, etc.

Does nothing if the user requested zero results, ie /posts?page=0

First resolves the top-level resource, then processes each relevant sideload

@see Resource#resolve @return [Array] an array of resolved model instances

# File lib/jsonapi_compliable/scope.rb, line 62
def resolve
  if @query.zero_results?
    []
  else
    resolved = @resource.resolve(@object)
    yield resolved if block_given?
    sideload(resolved, query_hash[:include]) if query_hash[:include]
    resolved
  end
end
resolve_stats() click to toggle source

Resolve the requested stats. Returns hash like:

{ rating: { average: 5.5, maximum: 9 } }

@return [Hash] the resolved stat info @api private

# File lib/jsonapi_compliable/scope.rb, line 49
def resolve_stats
  Stats::Payload.new(@resource, query_hash, @unpaginated_object).generate
end

Private Instance Methods

add_scoping(key, scoping_class, opts, default = {}) click to toggle source
# File lib/jsonapi_compliable/scope.rb, line 135
def add_scoping(key, scoping_class, opts, default = {})
  @object = scoping_class.new(@resource, query_hash, @object, default).apply unless opts[key] == false
  @unpaginated_object = @object unless key == :paginate
end
apply_scoping(opts) click to toggle source
# File lib/jsonapi_compliable/scope.rb, line 127
 def apply_scoping(opts)
  add_scoping(nil, JsonapiCompliable::Scoping::DefaultFilter, opts)
  add_scoping(:filter, JsonapiCompliable::Scoping::Filter, opts)
  add_scoping(:extra_fields, JsonapiCompliable::Scoping::ExtraFields, opts)
  add_scoping(:sort, JsonapiCompliable::Scoping::Sort, opts)
  add_scoping(:paginate, JsonapiCompliable::Scoping::Paginate, opts, default: opts[:default_paginate])
end
sideload(results, includes) click to toggle source
# File lib/jsonapi_compliable/scope.rb, line 82
def sideload(results, includes)
  return if results == []

  concurrent = ::JsonapiCompliable.config.experimental_concurrency
  promises = []

  includes.each_pair do |name, nested|
    sideload = @resource.sideload(name)

    if sideload.nil?
      if JsonapiCompliable.config.raise_on_missing_sideload
        raise JsonapiCompliable::Errors::InvalidInclude
          .new(name, @resource.type)
      end
    else
      namespace = Util::Sideload.namespace(@namespace, sideload.name)
      resolve_sideload = -> {
        begin
          sideload.resolve(results, @query, namespace)
        ensure
          ActiveRecord::Base.clear_active_connections! if defined?(ActiveRecord)
        end
      }
      if concurrent
        promises << Concurrent::Promise.execute(&resolve_sideload)
      else
        resolve_sideload.call
      end
    end
  end

  if concurrent
    # Wait for all promises to finish
    while !promises.all? { |p| p.fulfilled? || p.rejected? }
      sleep 0.01
    end
    # Re-raise the error with correct stacktrace
    # OPTION** to avoid failing here?? if so need serializable patch
    # to avoid loading data when association not loaded
    if rejected = promises.find(&:rejected?)
      raise rejected.reason
    end
  end
end