Class: JsonapiCompliable::Sideload

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

Constant Summary

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

Instance Attribute 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_procProc (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_keySymbol (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_fieldSymbol (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

#nameSymbol (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

#parentObject (readonly)

Returns the value of attribute parent



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

def parent
  @parent
end

#polymorphicBoolean (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_groupsHash (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_keySymbol (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_classClass (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_procProc (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

#sideloadsHash (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

#typeSymbol (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

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:



217
218
219
220
221
222
223
224
# 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_sideloadsObject

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.



333
334
335
336
337
338
339
340
341
342
343
# 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) ⇒ 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:



313
314
315
316
317
318
319
320
321
322
# 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 {|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:



168
169
170
# File 'lib/jsonapi_compliable/sideload.rb', line 168

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:



178
179
180
181
# 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 = []) ⇒ Object



345
346
347
348
349
350
351
352
353
354
# 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) ⇒ 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:



189
190
191
192
# 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) ⇒ Object



363
364
365
366
367
368
369
370
# 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) ⇒ 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:



248
249
250
# File 'lib/jsonapi_compliable/sideload.rb', line 248

def group_by(grouping_field)
  @grouping_field = grouping_field
end

#hooksObject

Get the hooks the user has configured

Returns:

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

See Also:



229
230
231
232
233
234
235
236
# 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?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:



91
92
93
# File 'lib/jsonapi_compliable/sideload.rb', line 91

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.



357
358
359
360
361
# 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) ⇒ 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)

    The current namespace (see Resource#with_context)

See Also:



267
268
269
270
271
272
273
# 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

#resourceResource

Returns an instance of #resource_class

Returns:

  • (Resource)

    an instance of #resource_class

See Also:



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

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:



131
132
133
# File 'lib/jsonapi_compliable/sideload.rb', line 131

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


328
329
330
# File 'lib/jsonapi_compliable/sideload.rb', line 328

def sideload(name)
  @sideloads[name]
end