class JsonapiCompliable::Util::Persistence
Save the given Resource#model, and all of its nested relationships. @api private
Public Class Methods
@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
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
# File lib/jsonapi_compliable/util/persistence.rb, line 59 def add_hook(prc) ::JsonapiCompliable::Util::Hooks.add(prc) end
# 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
# 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
# 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
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
# 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
# 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
# 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
# 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
# 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
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
# 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
# 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