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
Public Class Methods
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
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
@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
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
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
# 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
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
# 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
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
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
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
@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 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
@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
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
Private Instance Methods
# 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
# 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
# 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