From 268499bf400df6bad30f218f296905abdf8c9095 Mon Sep 17 00:00:00 2001 From: Samuel J Clopton Date: Fri, 18 Dec 2020 11:22:14 -0800 Subject: [PATCH 1/2] Initial implemention of `{ : { data: null } }` nullification This commit updates graphiti to be able to nullify relations only if `null` is specified in the assocation payload. This will still need feedback from the library maintainers, and after approval of design tests will need written to test this feature. --- .../adapters/persistence/associations.rb | 25 +++++++++-- lib/graphiti/deserializer.rb | 8 ++++ lib/graphiti/util/persistence.rb | 4 +- lib/graphiti/util/relationship_payload.rb | 44 +++++++++++++++---- 4 files changed, 68 insertions(+), 13 deletions(-) diff --git a/lib/graphiti/adapters/persistence/associations.rb b/lib/graphiti/adapters/persistence/associations.rb index 444c8344..8166cdb0 100644 --- a/lib/graphiti/adapters/persistence/associations.rb +++ b/lib/graphiti/adapters/persistence/associations.rb @@ -2,9 +2,28 @@ module Graphiti module Adapters module Persistence module Associations + def process_nullified_belongs_to_associations(persistence, attributes) + persistence.iterate(only: { relationship_types: [:polymorphic_belongs_to, :belongs_to], method_types: [:nullify] }) do |x| + update_foreign_key(persistence, attributes, x) + end + end + + def process_nullified_has_many_associations(persistence, caller_model) + [].tap do |processed| + persistence.iterate(only: { method_types: [:nullify] }, except: { relationship_types: [:polymorphic_belongs_to, :belongs_to] }) do |x| + update_foreign_key(caller_model, x[:attributes], x) + + x[:object] = x[:resource] + .persist_with_relationships(x[:meta], x[:attributes], x[:relationships], caller_model, x[:foreign_key]) + + processed << x + end + end + end + def process_belongs_to(persistence, attributes) parents = [].tap do |processed| - persistence.iterate(only: [:polymorphic_belongs_to, :belongs_to]) do |x| + persistence.iterate(only: { relationship_types: [:polymorphic_belongs_to, :belongs_to] }, except: { method_types: [:nullify] }) do |x| begin id = x.dig(:attributes, :id) x[:object] = x[:resource] @@ -23,7 +42,7 @@ def process_belongs_to(persistence, attributes) def process_has_many(persistence, caller_model) [].tap do |processed| - persistence.iterate(except: [:polymorphic_belongs_to, :belongs_to]) do |x| + persistence.iterate(except: { relationship_types: [:polymorphic_belongs_to, :belongs_to], method_types: [:nullify] }) do |x| update_foreign_key(caller_model, x[:attributes], x) x[:object] = x[:resource] @@ -47,7 +66,7 @@ def update_foreign_key_for_parents(parents, attributes) def update_foreign_key(parent_object, attrs, x) return if x[:sideload].type == :many_to_many - if [:destroy, :disassociate].include?(x[:meta][:method]) + if [:destroy, :disassociate, :nullify].include?(x[:meta][:method]) if x[:sideload].polymorphic_has_one? || x[:sideload].polymorphic_has_many? attrs[:"#{x[:sideload].polymorphic_as}_type"] = nil end diff --git a/lib/graphiti/deserializer.rb b/lib/graphiti/deserializer.rb index dc7ce8c2..adea3da1 100644 --- a/lib/graphiti/deserializer.rb +++ b/lib/graphiti/deserializer.rb @@ -146,6 +146,14 @@ def process_relationships(relationship_hash) if relationship_payload[:data] hash[name] = process_relationship(relationship_payload[:data]) + elsif relationship_payload.key?(:data) && relationship_payload[:data] == nil + hash[name] = { + meta: { + method: :nullify + }, + attributes: {}, + relationships: {} + } end end end diff --git a/lib/graphiti/util/persistence.rb b/lib/graphiti/util/persistence.rb index 05e63723..40197f7d 100644 --- a/lib/graphiti/util/persistence.rb +++ b/lib/graphiti/util/persistence.rb @@ -45,6 +45,7 @@ def initialize(resource, meta, attributes, relationships, caller_model, foreign_ # @return a model instance def run attributes = @adapter.persistence_attributes(self, @attributes) + @adapter.process_nullified_belongs_to_associations(self, attributes) parents = @adapter.process_belongs_to(self, attributes) persisted = persist_object(@meta[:method], attributes) @@ -53,6 +54,7 @@ def run associate_parents(persisted, parents) + @adapter.process_nullified_has_many_associations(self, persisted) children = @adapter.process_has_many(self, persisted) associate_children(persisted, children) unless @meta[:method] == :destroy @@ -68,7 +70,7 @@ def run persisted end - def iterate(only: [], except: []) + def iterate(only: {}, except: {}) opts = { resource: @resource, relationships: @relationships diff --git a/lib/graphiti/util/relationship_payload.rb b/lib/graphiti/util/relationship_payload.rb index 3fcb3bbe..b8d7dfc0 100644 --- a/lib/graphiti/util/relationship_payload.rb +++ b/lib/graphiti/util/relationship_payload.rb @@ -5,14 +5,14 @@ module Util class RelationshipPayload attr_reader :resource, :payload - def self.iterate(resource:, relationships: {}, only: [], except: []) + def self.iterate(resource:, relationships: {}, only: {}, except: {}) instance = new(resource, relationships, only: only, except: except) instance.iterate do |sideload, relationship_data, sub_relationships| yield sideload, relationship_data, sub_relationships end end - def initialize(resource, payload, only: [], except: []) + def initialize(resource, payload, only: {}, except: {}) @resource = resource @payload = payload @only = only @@ -22,13 +22,17 @@ def initialize(resource, payload, only: [], except: []) def iterate payload.each_pair do |relationship_name, relationship_payload| if (sl = resource.class.sideload(relationship_name.to_sym)) - if should_yield?(sl.type) + if should_yield_relationship_type?(sl.type) if relationship_payload.is_a?(Array) relationship_payload.each do |rp| - yield payload_for(sl, rp) + if should_yield_method_type?(rp.fetch(:meta, {})[:method] || :update) + yield payload_for(sl, rp) + end end else - yield payload_for(sl, relationship_payload) + if should_yield_method_type?(relationship_payload.fetch(:meta, {})[:method] || :update) + yield payload_for(sl, relationship_payload) + end end end end @@ -37,10 +41,32 @@ def iterate private - def should_yield?(type) - (@only.length == 0 && @except.length == 0) || - (@only.length > 0 && @only.include?(type)) || - (@except.length > 0 && !@except.include?(type)) + def only_relationship_types + @only[:relationship_types] || [] + end + + def except_relationship_types + @except[:relationship_types] || [] + end + + def only_method_types + @only[:method_types] || [] + end + + def except_method_types + @except[:method_types] || [] + end + + def should_yield_method_type?(type) + (only_method_types.length == 0 && except_method_types.length == 0) || + (only_method_types.length > 0 && only_method_types.include?(type)) || + (except_method_types.length > 0 && !except_method_types.include?(type)) + end + + def should_yield_relationship_type?(type) + (only_relationship_types.length == 0 && except_relationship_types.length == 0) || + (only_relationship_types.length > 0 && only_relationship_types.include?(type)) || + (except_relationship_types.length > 0 && !except_relationship_types.include?(type)) end def payload_for(sideload, relationship_payload) From 10cde1e6fc93d40b88fe02e5ea85e69f7df6ceb4 Mon Sep 17 00:00:00 2001 From: Samuel J Clopton Date: Fri, 18 Dec 2020 13:25:02 -0800 Subject: [PATCH 2/2] ... --- lib/graphiti/util/relationship_payload.rb | 59 ++++++++++++++--------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/lib/graphiti/util/relationship_payload.rb b/lib/graphiti/util/relationship_payload.rb index b8d7dfc0..eb869161 100644 --- a/lib/graphiti/util/relationship_payload.rb +++ b/lib/graphiti/util/relationship_payload.rb @@ -70,31 +70,46 @@ def should_yield_relationship_type?(type) end def payload_for(sideload, relationship_payload) - type = relationship_payload[:meta][:jsonapi_type].to_sym + if relationship_payload[:meta][:method] == :nullify + resource = sideload.resource - # For polymorphic *sideloads*, grab the correct child sideload - if sideload.resource.type != type && sideload.type == :polymorphic_belongs_to - sideload = sideload.child_for_type!(type) - end + { + resource: resource, + sideload: sideload, + is_polymorphic: sideload.polymorphic_child?, + primary_key: sideload.primary_key, + foreign_key: sideload.foreign_key, + attributes: relationship_payload[:attributes], + meta: relationship_payload[:meta], + relationships: relationship_payload[:relationships] + } + else + type = relationship_payload[:meta][:jsonapi_type].to_sym - # For polymorphic *resources*, grab the correct child resource - resource = sideload.resource - if resource.type != type && resource.polymorphic? - resource = resource.class.resource_for_type(type).new - end + # For polymorphic *sideloads*, grab the correct child sideload + if sideload.resource.type != type && sideload.type == :polymorphic_belongs_to + sideload = sideload.child_for_type!(type) + end - relationship_payload[:meta][:method] ||= :update - - { - resource: resource, - sideload: sideload, - is_polymorphic: sideload.polymorphic_child?, - primary_key: sideload.primary_key, - foreign_key: sideload.foreign_key, - attributes: relationship_payload[:attributes], - meta: relationship_payload[:meta], - relationships: relationship_payload[:relationships] - } + # For polymorphic *resources*, grab the correct child resource + resource = sideload.resource + if resource.type != type && resource.polymorphic? + resource = resource.class.resource_for_type(type).new + end + + relationship_payload[:meta][:method] ||= :update + + { + resource: resource, + sideload: sideload, + is_polymorphic: sideload.polymorphic_child?, + primary_key: sideload.primary_key, + foreign_key: sideload.foreign_key, + attributes: relationship_payload[:attributes], + meta: relationship_payload[:meta], + relationships: relationship_payload[:relationships] + } + end end end end