class JsonapiCompliable::Sideload

@attr_reader [Symbol] name The name of the sideload @attr_reader [Class] resource_class The corresponding Resource class @attr_reader [Boolean] polymorphic Is this a polymorphic sideload? @attr_reader [Hash] polymorphic_groups The subgroups, when polymorphic @attr_reader [Hash] sideloads The associated sibling sideloads @attr_reader [Proc] scope_proc The configured 'scope' block @attr_reader [Proc] assign_proc The configured 'assign' block @attr_reader [Symbol] grouping_field The configured 'group_by' symbol @attr_reader [Symbol] foreign_key The attribute used to match objects - need not be a true database foreign key. @attr_reader [Symbol] primary_key The attribute used to match objects - need not be a true database primary key. @attr_reader [Symbol] type One of :has_many, :belongs_to, etc

Constants

HOOK_ACTIONS

Attributes

assign_proc[R]
foreign_key[R]
grouping_field[R]
name[R]
parent[R]
polymorphic[R]
polymorphic_groups[R]
primary_key[R]
resource_class[R]
scope_proc[R]
sideloads[R]
type[R]

Public Class Methods

new(name, type: nil, resource: nil, polymorphic: false, primary_key: :id, foreign_key: nil, parent: nil) click to toggle source

NB - the adapter's #sideloading_module is mixed in on instantiation

An anonymous Resource will be assigned when none provided.

@see Adapters::Abstract#sideloading_module

# File lib/jsonapi_compliable/sideload.rb, line 32
def initialize(name, type: nil, resource: nil, polymorphic: false, primary_key: :id, foreign_key: nil, parent: nil)
  @name               = name
  @resource_class     = (resource || Class.new(Resource))
  @sideloads          = {}
  @polymorphic        = !!polymorphic
  @polymorphic_groups = {} if polymorphic?
  @parent             = parent
  @primary_key        = primary_key
  @foreign_key        = foreign_key
  @type               = type

  extend @resource_class.config[:adapter].sideloading_module
end

Public Instance Methods

after_save(only: [], except: [], &blk) click to toggle source

Configure post-processing hooks

In particular, helpful for bulk operations. “after_save” will fire for any persistence method - :create, :update, :destroy, :disassociate. Use “only” and “except” keyword arguments to fire only for a specific persistence method.

@example Bulk Notify Users on Invite

class ProjectResource < ApplicationResource
  # ... code ...
  allow_sideload :users, resource: UserResource do
    # scope {}
    # assign {}
    after_save only: [:create] do |project, users|
      UserMailer.invite(project, users).deliver_later
    end
  end
end

@see hooks @see Util::Persistence

# File lib/jsonapi_compliable/sideload.rb, line 217
def after_save(only: [], except: [], &blk)
  actions = HOOK_ACTIONS - except
  actions = only & actions
  actions = [:save] if only.empty? && except.empty?
  actions.each do |a|
    hooks[:"after_#{a}"] << blk
  end
end
all_sideloads() click to toggle source

@api private

# File lib/jsonapi_compliable/sideload.rb, line 333
def all_sideloads
  {}.tap do |all|
    if polymorphic?
      polymorphic_groups.each_pair do |type, sl|
        all.merge!(sl.resource.sideloading.all_sideloads)
      end
    else
      all.merge!(@sideloads.merge(resource.sideloading.sideloads))
    end
  end
end
allow_sideload(name, opts = {}, &blk) click to toggle source

Configure a relationship between Resource objects

You probably want to extract this logic into an adapter rather than using directly

@example Default ActiveRecord

# What happens 'under the hood'
class CommentResource < ApplicationResource
  # ... code ...
  allow_sideload :post, resource: PostResource do
    scope do |comments|
      Post.where(id: comments.map(&:post_id))
    end

    assign do |comments, posts|
      comments.each do |comment|
        relevant_post = posts.find { |p| p.id == comment.post_id }
        comment.post = relevant_post
      end
    end
  end
end

# Rather than writing that code directly, go through the adapter:
class CommentResource < ApplicationResource
  # ... code ...
  use_adapter JsonapiCompliable::Adapters::ActiveRecord

  belongs_to :post,
    scope: -> { Post.all },
    resource: PostResource,
    foreign_key: :post_id
end

@see Adapters::ActiveRecordSideloading#belongs_to @see assign @see scope @return void

# File lib/jsonapi_compliable/sideload.rb, line 313
def allow_sideload(name, opts = {}, &blk)
  sideload = Sideload.new(name, opts)
  sideload.instance_eval(&blk) if blk

  if polymorphic?
    @polymorphic_groups[name] = sideload
  else
    @sideloads[name] = sideload
  end
end
assign(&blk) click to toggle source

The proc used to assign the resolved parents and children.

You probably want to wrap this logic in an Adapter, instead of specifying in your resource directly.

@example Default ActiveRecord

class PostResource < ApplicationResource
  # ... code ...
  allow_sideload :comments, resource: CommentResource do
    # ... code ...
    assign do |posts, comments|
      posts.each do |post|
        relevant_comments = comments.select { |c| c.post_id == post.id }
        post.comments = relevant_comments
      end
    end
  end
end

@example ActiveRecord via Adapter

class PostResource < ApplicationResource
  # ... code ...
  has_many :comments,
    scope: -> { Comment.all },
    resource: CommentResource,
    foreign_key: :post_id
end

@see Adapters::Abstract @see Adapters::ActiveRecordSideloading#has_many @see allow_sideload @yieldparam parents - The resolved parent records @yieldparam children - The resolved child records

# File lib/jsonapi_compliable/sideload.rb, line 168
def assign(&blk)
  @assign_proc = blk
end
associate(parent, child) click to toggle source

Configure how to associate parent and child records. Delegates to resource

@see name @see type @api private

# File lib/jsonapi_compliable/sideload.rb, line 178
def associate(parent, child)
  association_name = @parent ? @parent.name : name
  resource.associate(parent, child, association_name, type)
end
association_names(memo = []) click to toggle source
# File lib/jsonapi_compliable/sideload.rb, line 345
def association_names(memo = [])
  all_sideloads.each_pair do |name, sl|
    unless memo.include?(sl.name)
      memo << sl.name
      memo |= sl.association_names(memo)
    end
  end

  memo
end
disassociate(parent, child) click to toggle source

Configure how to disassociate parent and child records. Delegates to resource

@see name @see type @api private

# File lib/jsonapi_compliable/sideload.rb, line 189
def disassociate(parent, child)
  association_name = @parent ? @parent.name : name
  resource.disassociate(parent, child, association_name, type)
end
fire_hooks!(parent, objects, method) click to toggle source
# File lib/jsonapi_compliable/sideload.rb, line 363
def fire_hooks!(parent, objects, method)
  return unless self.hooks

  hooks = self.hooks[:"after_#{method}"] + self.hooks[:after_save]
  hooks.compact.each do |hook|
    resource.instance_exec(parent, objects, &hook)
  end
end
group_by(grouping_field) click to toggle source

Define an attribute that groups the parent records. For instance, with an ActiveRecord polymorphic belongs_to there will be a parent_id and parent_type. We would want to group on parent_type:

allow_sideload :organization, polymorphic: true do
  # group parent_type, parent here is 'organization'
  group_by :organization_type
end

@see polymorphic?

# File lib/jsonapi_compliable/sideload.rb, line 248
def group_by(grouping_field)
  @grouping_field = grouping_field
end
hooks() click to toggle source

Get the hooks the user has configured @see after_save @return hash of hooks, ie +{ after_create: #<Proc>}+

# File lib/jsonapi_compliable/sideload.rb, line 229
def hooks
  @hooks ||= {}.tap do |h|
    HOOK_ACTIONS.each do |a|
      h[:"after_#{a}"] = []
      h[:"before_#{a}"] = []
    end
  end
end
polymorphic?() click to toggle source

Is this sideload polymorphic?

Polymorphic sideloads group the parent objects in some fashion, so different 'types' can be resolved differently. Let's say an Office has a polymorphic Organization, which can be either a Business or Government:

allow_sideload :organization, :polymorphic: true do
  group_by :organization_type

  allow_sideload 'Business', resource: BusinessResource do
    # ... code ...
  end

  allow_sideload 'Governemnt', resource: GovernmentResource do
    # ... code ...
  end
end

You probably want to extract this code into an Adapter. For instance, with ActiveRecord:

polymorphic_belongs_to :organization,
  group_by: :organization_type,
  groups: {
    'Business' => {
      scope: -> { Business.all },
      resource: BusinessResource,
      foreign_key: :organization_id
    },
    'Government' => {
      scope: -> { Government.all },
      resource: GovernmentResource,
      foreign_key: :organization_id
    }
  }

@see Adapters::ActiveRecordSideloading#polymorphic_belongs_to @return [Boolean] is this sideload polymorphic?

# File lib/jsonapi_compliable/sideload.rb, line 91
def polymorphic?
  @polymorphic == true
end
polymorphic_child_for_type(type) click to toggle source

@api private

# File lib/jsonapi_compliable/sideload.rb, line 357
def polymorphic_child_for_type(type)
  polymorphic_groups.values.find do |v|
    v.resource_class.config[:type] == type.to_sym
  end
end
resolve(parents, query, namespace) click to toggle source

Resolve the sideload.

  • Uses the 'scope' proc to build a 'base scope'

  • Chains additional criteria onto that 'base scope'

  • Resolves that scope (see Scope#resolve)

  • Assigns the resulting child objects to their corresponding parents

@see Scope#resolve @param [Object] parents The resolved parent models @param [Query] query The Query instance @param [Symbol] namespace The current namespace (see Resource#with_context) @see Query @see Resource#with_context @return [void] @api private

# File lib/jsonapi_compliable/sideload.rb, line 267
def resolve(parents, query, namespace)
  if polymorphic?
    resolve_polymorphic(parents, query)
  else
    resolve_basic(parents, query, namespace)
  end
end
resource() click to toggle source

@see resource_class @return [Resource] an instance of #resource_class

# File lib/jsonapi_compliable/sideload.rb, line 48
def resource
  @resource ||= resource_class.new
end
scope(&blk) click to toggle source

Build a scope that will be used to fetch the related records This scope will be further chained with filtering/sorting/etc

You probably want to wrap this logic in an Adapter, instead of specifying in your resource directly.

@example Default ActiveRecord

class PostResource < ApplicationResource
  # ... code ...
  allow_sideload :comments, resource: CommentResource do
    scope do |posts|
      Comment.where(post_id: posts.map(&:id))
    end
    # ... code ...
  end
end

@example Custom Scope

# In this example, our base scope is a Hash
scope do |posts|
  { post_ids: posts.map(&:id) }
end

@example ActiveRecord via Adapter

class PostResource < ApplicationResource
  # ... code ...
  has_many :comments,
    scope: -> { Comment.all },
    resource: CommentResource,
    foreign_key: :post_id
end

@see Adapters::Abstract @see Adapters::ActiveRecordSideloading#has_many @see allow_sideload @yieldparam parents - The resolved parent records

# File lib/jsonapi_compliable/sideload.rb, line 131
def scope(&blk)
  @scope_proc = blk
end
sideload(name) click to toggle source

Fetch a Sideload object by its name @param [Symbol] name The name of the corresponding sideload @see +allow_sideload @return the corresponding Sideload object

# File lib/jsonapi_compliable/sideload.rb, line 328
def sideload(name)
  @sideloads[name]
end

Private Instance Methods

polymorphic_grouper(grouping_field) click to toggle source
# File lib/jsonapi_compliable/sideload.rb, line 374
def polymorphic_grouper(grouping_field)
  lambda do |record|
    if record.is_a?(Hash)
      if record.keys[0].is_a?(Symbol)
        record[grouping_field]
      else
        record[grouping_field.to_s]
      end
    else
      record.send(grouping_field)
    end
  end
end
resolve_basic(parents, query, namespace) click to toggle source
# File lib/jsonapi_compliable/sideload.rb, line 399
def resolve_basic(parents, query, namespace)
  sideload_scope   = scope_proc.call(parents)
  sideload_scope   = Scope.new(sideload_scope, resource_class.new, query, default_paginate: false, namespace: namespace)
  sideload_scope.resolve do |sideload_results|
    assign_proc.call(parents, sideload_results)
  end
end
resolve_polymorphic(parents, query) click to toggle source
# File lib/jsonapi_compliable/sideload.rb, line 388
def resolve_polymorphic(parents, query)
  grouper = polymorphic_grouper(@grouping_field)

  parents.group_by(&grouper).each_pair do |group_type, group_members|
    sideload_for_group = @polymorphic_groups[group_type]
    if sideload_for_group
      sideload_for_group.resolve(group_members, query, name)
    end
  end
end