Class: JsonapiCompliable::Sideload

Inherits:
Object
  • Object
show all
Defined in:
lib/jsonapi_compliable/sideload.rb

Constant Summary collapse

HOOK_ACTIONS =
[:save, :create, :update, :destroy, :disassociate]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, type: nil, resource: nil, polymorphic: false, primary_key: :id, foreign_key: nil, parent: nil) ⇒ Sideload

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

An anonymous Resource will be assigned when none provided.



32
33
34
35
36
37
38
39
40
41
42
43
44
# 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

Instance Attribute Details

#assign_proc ⇒ Proc (readonly)

The configured 'assign' block

Returns:

  • (Proc) —

    the current value of assign_proc



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def assign_proc
  @assign_proc
end

#foreign_key ⇒ Symbol (readonly)

The attribute used to match objects - need not be a true database foreign key.

Returns:

  • (Symbol) —

    the current value of foreign_key



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def foreign_key
  @foreign_key
end

#grouping_field ⇒ Symbol (readonly)

The configured 'group_by' symbol

Returns:

  • (Symbol) —

    the current value of grouping_field



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def grouping_field
  @grouping_field
end

#name ⇒ Symbol (readonly)

The name of the sideload

Returns:

  • (Symbol) —

    the current value of name



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def name
  @name
end

#parent ⇒ Object (readonly)

Returns the value of attribute parent.



14
15
16
# File 'lib/jsonapi_compliable/sideload.rb', line 14

def parent
  @parent
end

#polymorphic ⇒ Boolean (readonly)

Is this a polymorphic sideload?

Returns:

  • (Boolean) —

    the current value of polymorphic



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def polymorphic
  @polymorphic
end

#polymorphic_groups ⇒ Hash (readonly)

The subgroups, when polymorphic

Returns:

  • (Hash) —

    the current value of polymorphic_groups



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def polymorphic_groups
  @polymorphic_groups
end

#primary_key ⇒ Symbol (readonly)

The attribute used to match objects - need not be a true database primary key.

Returns:

  • (Symbol) —

    the current value of primary_key



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def primary_key
  @primary_key
end

#resource_class ⇒ Class (readonly)

The corresponding Resource class

Returns:

  • (Class) —

    the current value of resource_class



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def resource_class
  @resource_class
end

#scope_proc ⇒ Proc (readonly)

The configured 'scope' block

Returns:

  • (Proc) —

    the current value of scope_proc



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def scope_proc
  @scope_proc
end

#sideloads ⇒ Hash (readonly)

The associated sibling sideloads

Returns:

  • (Hash) —

    the current value of sideloads



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def sideloads
  @sideloads
end

#type ⇒ Symbol (readonly)

One of :has_many, :belongs_to, etc

Returns:

  • (Symbol) —

    the current value of type



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def type
  @type
end

Class Method Details

.max_recursion ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



47
48
49
# File 'lib/jsonapi_compliable/sideload.rb', line 47

def self.max_recursion
  @max_recursion || 2
end

.max_recursion=(val) ⇒ Object

Set maximum levels of sideload recursion /authors?comments.authors would be one level /authors?comments.authors.comments.authors would be two levels etc

Default max recursion is 2



57
58
59
# File 'lib/jsonapi_compliable/sideload.rb', line 57

def self.max_recursion=(val)
  @max_recursion = val
end

Instance Method Details

#after_save(only: [], except: [], &blk) ⇒ Object

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.

Examples:

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 Also:



232
233
234
235
236
237
238
239
# File 'lib/jsonapi_compliable/sideload.rb', line 232

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 ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



350
351
352
353
354
355
356
357
358
359
360
# File 'lib/jsonapi_compliable/sideload.rb', line 350

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) ⇒ Object

Configure a relationship between Resource objects

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

Examples:

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

Returns:

  • void

See Also:



330
331
332
333
334
335
336
337
338
339
# File 'lib/jsonapi_compliable/sideload.rb', line 330

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 {|parents, children| ... } ⇒ Object

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.

Examples:

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

ActiveRecord via Adapter

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

Yield Parameters:

  • parents —
    • The resolved parent records
  • children —
    • The resolved child records

See Also:



183
184
185
# File 'lib/jsonapi_compliable/sideload.rb', line 183

def assign(&blk)
  @assign_proc = blk
end

#associate(parent, child) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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

See Also:



193
194
195
196
# File 'lib/jsonapi_compliable/sideload.rb', line 193

def associate(parent, child)
  association_name = @parent ? @parent.name : name
  resource.associate(parent, child, association_name, type)
end

#disassociate(parent, child) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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

See Also:



204
205
206
207
# File 'lib/jsonapi_compliable/sideload.rb', line 204

def disassociate(parent, child)
  association_name = @parent ? @parent.name : name
  resource.disassociate(parent, child, association_name, type)
end

#fire_hooks!(parent, objects, method) ⇒ Object



409
410
411
412
413
414
415
416
# File 'lib/jsonapi_compliable/sideload.rb', line 409

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) ⇒ Object

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 Also:



263
264
265
# File 'lib/jsonapi_compliable/sideload.rb', line 263

def group_by(grouping_field)
  @grouping_field = grouping_field
end

#hooks ⇒ Object

Get the hooks the user has configured

Returns:

  • hash of hooks, ie { after_create: #<Proc>}

See Also:



244
245
246
247
248
249
250
251
# File 'lib/jsonapi_compliable/sideload.rb', line 244

def hooks
  @hooks ||= {}.tap do |h|
    HOOK_ACTIONS.each do |a|
      h[:"after_#{a}"] = []
      h[:"before_#{a}"] = []
    end
  end
end

#polymorphic? ⇒ Boolean

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
  }
}

Returns:

  • (Boolean) —

    is this sideload polymorphic?

See Also:



106
107
108
# File 'lib/jsonapi_compliable/sideload.rb', line 106

def polymorphic?
  @polymorphic == true
end

#polymorphic_child_for_type(type) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



403
404
405
406
407
# File 'lib/jsonapi_compliable/sideload.rb', line 403

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 = nil) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

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

Parameters:

  • parents (Object) —

    The resolved parent models

  • query (Query) —

    The Query instance

  • namespace (Symbol) (defaults to: nil) —

    The current namespace (see Resource#with_context)

See Also:



282
283
284
285
286
287
288
289
290
# File 'lib/jsonapi_compliable/sideload.rb', line 282

def resolve(parents, query, namespace = nil)
  namespace ||= name

  if polymorphic?
    resolve_polymorphic(parents, query)
  else
    resolve_basic(parents, query, namespace)
  end
end

#resource ⇒ Resource

Returns an instance of #resource_class.

Returns:

  • (Resource) —

    an instance of #resource_class

See Also:



63
64
65
# File 'lib/jsonapi_compliable/sideload.rb', line 63

def resource
  @resource ||= resource_class.new
end

#scope {|parents| ... } ⇒ Object

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.

Examples:

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

Custom Scope

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

ActiveRecord via Adapter

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

Yield Parameters:

  • parents —
    • The resolved parent records

See Also:



146
147
148
# File 'lib/jsonapi_compliable/sideload.rb', line 146

def scope(&blk)
  @scope_proc = blk
end

#sideload(name) ⇒ Object

Fetch a Sideload object by its name

Parameters:

  • name (Symbol) —

    The name of the corresponding sideload

Returns:

  • the corresponding Sideload object

See Also:

  • +allow_sideload


345
346
347
# File 'lib/jsonapi_compliable/sideload.rb', line 345

def sideload(name)
  @sideloads[name]
end

#to_hash(recursion_chain = [], parent = nil) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Looks at all nested sideload, and all nested sideloads for the corresponding Resources, and returns an Include Directive hash

For instance, this configuration:

class BarResource < ApplicationResource
allow_sideload :baz do
end
end

class PostResource < ApplicationResource
allow_sideload :foo do
  allow_sideload :bar, resource: BarResource do
  end
end
end

post_resource.sideloading.to_hash would return

{ base: { foo: { bar: { baz: {} } } } }

Returns:

  • (Hash) —

    The nested include hash



385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/jsonapi_compliable/sideload.rb', line 385

def to_hash(recursion_chain = [], parent = nil)
  recursing = ->(arr) { arr == [parent.object_id, self.object_id] }
  recursions = recursion_chain.select(&recursing).length
  return {} if recursions >= self.class.max_recursion

  unless (parent && parent.name == :base) || name == :base
    recursion_chain += [[parent.object_id, self.object_id]]
  end

  { name => {} }.tap do |hash|
    all_sideloads.each_pair do |key, sl|
      sideload_hash = sl.to_hash(recursion_chain, self)
      hash[name].merge!(sideload_hash)
    end
  end
end