class JsonapiCompliable::Util::Persistence

Save the given Resource#model, and all of its nested relationships. @api private

Public Class Methods

new(resource, meta, attributes, relationships, caller_model) click to toggle source

@param [Resource] resource the resource instance @param [Hash] meta see (Deserializer#meta) @param [Hash] attributes see (Deserializer#attributes) @param [Hash] relationships see (Deserializer#relationships)

# File lib/jsonapi_compliable/util/persistence.rb, line 8
def initialize(resource, meta, attributes, relationships, caller_model)
  @resource      = resource
  @meta          = meta
  @attributes    = attributes
  @relationships = relationships
  @caller_model  = caller_model
end

Public Instance Methods

run() click to toggle source

Perform the actual save logic.

belongs_to must be processed before/separately from has_many - we need to know the primary key value of the parent before persisting the child.

Flow:

  • process parents

  • update attributes to reflect parent primary keys

  • persist current object

  • associate temp id with current object

  • associate parent objects with current object

  • process children

  • associate children

  • record hooks for later playback

  • run post-process sideload hooks

  • return current object

@return a model instance

# File lib/jsonapi_compliable/util/persistence.rb, line 35
def run
  parents = process_belongs_to(@relationships)
  update_foreign_key_for_parents(parents)

  persisted = persist_object(@meta[:method], @attributes)
  assign_temp_id(persisted, @meta[:temp_id])

  associate_parents(persisted, parents)

  children = process_has_many(@relationships, persisted) do |x|
    update_foreign_key(persisted, x[:attributes], x)
  end

  associate_children(persisted, children) unless @meta[:method] == :destroy

  post_process(persisted, parents)
  post_process(persisted, children)
  before_commit = -> { @resource.before_commit(persisted, @meta[:method]) }
  add_hook(before_commit)
  persisted
end

Private Instance Methods

add_hook(prc) click to toggle source
# File lib/jsonapi_compliable/util/persistence.rb, line 59
def add_hook(prc)
  ::JsonapiCompliable::Util::Hooks.add(prc)
end
assign_temp_id(object, temp_id) click to toggle source
# File lib/jsonapi_compliable/util/persistence.rb, line 164
def assign_temp_id(object, temp_id)
  object.instance_variable_set(:@_jsonapi_temp_id, temp_id)
end
associate_children(object, children) click to toggle source
# File lib/jsonapi_compliable/util/persistence.rb, line 105
def associate_children(object, children)
  children.each do |x|
    if x[:object] && object
      if x[:meta][:method] == :disassociate
        x[:sideload].disassociate(object, x[:object])
      elsif x[:meta][:method] == :destroy
        if x[:sideload].type == :habtm
          x[:sideload].disassociate(object, x[:object])
        end # otherwise, no need to disassociate destroyed objects
      else
        x[:sideload].associate(object, x[:object])
      end
    end
  end
end
associate_parents(object, parents) click to toggle source
# File lib/jsonapi_compliable/util/persistence.rb, line 90
def associate_parents(object, parents)
  # No need to associate to destroyed objects
  parents = parents.select { |x| x[:meta][:method] != :destroy }

  parents.each do |x|
    if x[:object] && object
      if x[:meta][:method] == :disassociate
        x[:sideload].disassociate(x[:object], object)
      else
        x[:sideload].associate(x[:object], object)
      end
    end
  end
end
call_resource_method(method_name, attributes, caller_model) click to toggle source

In the Resource, we want to allow:

def create(attrs)

and

def create(attrs, parent = nil)

'parent' is an optional parameter that should not be part of the method signature in most use cases.

# File lib/jsonapi_compliable/util/persistence.rb, line 189
def call_resource_method(method_name, attributes, caller_model)
  method = @resource.method(method_name)
  if [2,-2].include?(method.arity)
    method.call(attributes, caller_model)
  else
    method.call(attributes)
  end
end
iterate(only: [], except: []) { |x| ... } click to toggle source
# File lib/jsonapi_compliable/util/persistence.rb, line 168
def iterate(only: [], except: [])
  opts = {
    resource: @resource,
    relationships: @relationships,
  }.merge(only: only, except: except)

  JsonapiCompliable::Util::RelationshipPayload.iterate(opts) do |x|
    yield x
  end
end
persist_object(method, attributes) click to toggle source
# File lib/jsonapi_compliable/util/persistence.rb, line 121
def persist_object(method, attributes)
  case method
    when :destroy
      call_resource_method(:destroy, attributes[:id], @caller_model)
    when :update, nil, :disassociate
      call_resource_method(:update, attributes, @caller_model)
    else
      call_resource_method(:create, attributes, @caller_model)
  end
end
post_process(caller_model, processed) click to toggle source
# File lib/jsonapi_compliable/util/persistence.rb, line 153
def post_process(caller_model, processed)
  groups = processed.group_by { |x| x[:meta][:method] }
  groups.each_pair do |method, group|
    group.group_by { |g| g[:sideload] }.each_pair do |sideload, members|
      objects = members.map { |x| x[:object] }
      hook = -> { sideload.fire_hooks!(caller_model, objects, method) }
      add_hook(hook)
    end
  end
end
process_belongs_to(relationships) click to toggle source
# File lib/jsonapi_compliable/util/persistence.rb, line 143
def process_belongs_to(relationships)
  [].tap do |processed|
    iterate(only: [:polymorphic_belongs_to, :belongs_to]) do |x|
      x[:object] = x[:sideload].resource
        .persist_with_relationships(x[:meta], x[:attributes], x[:relationships])
      processed << x
    end
  end
end
process_has_many(relationships, caller_model) { |x| ... } click to toggle source
# File lib/jsonapi_compliable/util/persistence.rb, line 132
def process_has_many(relationships, caller_model)
  [].tap do |processed|
    iterate(except: [:polymorphic_belongs_to, :belongs_to]) do |x|
      yield x
      x[:object] = x[:sideload].resource
        .persist_with_relationships(x[:meta], x[:attributes], x[:relationships], caller_model)
      processed << x
    end
  end
end
update_foreign_key(parent_object, attrs, x) click to toggle source

The child's attributes should be modified to nil-out the foreign_key when the parent is being destroyed or disassociated

This is not the case for HABTM, whose “foreign key” is a join table

# File lib/jsonapi_compliable/util/persistence.rb, line 67
def update_foreign_key(parent_object, attrs, x)
  return if x[:sideload].type == :habtm

  if [:destroy, :disassociate].include?(x[:meta][:method])
    attrs[x[:foreign_key]] = nil
    update_foreign_type(attrs, x, null: true) if x[:is_polymorphic]
  else
    attrs[x[:foreign_key]] = parent_object.send(x[:primary_key])
    update_foreign_type(attrs, x) if x[:is_polymorphic]
  end
end
update_foreign_key_for_parents(parents) click to toggle source
# File lib/jsonapi_compliable/util/persistence.rb, line 84
def update_foreign_key_for_parents(parents)
  parents.each do |x|
    update_foreign_key(x[:object], @attributes, x)
  end
end
update_foreign_type(attrs, x, null: false) click to toggle source
# File lib/jsonapi_compliable/util/persistence.rb, line 79
def update_foreign_type(attrs, x, null: false)
  grouping_field = x[:sideload].parent.grouping_field
  attrs[grouping_field] = null ? nil : x[:sideload].name
end