diff --git a/graphiti.gemspec b/graphiti.gemspec index 83b6ab4d..13b65095 100644 --- a/graphiti.gemspec +++ b/graphiti.gemspec @@ -16,7 +16,7 @@ Gem::Specification.new do |spec| spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.required_ruby_version = [">= 2.3", "< 3.1"] + spec.required_ruby_version = [">= 2.3", "< 3.3"] spec.add_dependency "jsonapi-serializable", "~> 0.3.0" spec.add_dependency "jsonapi-renderer", "~> 0.2", ">= 0.2.2" @@ -29,7 +29,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "kaminari", "~> 0.17" spec.add_development_dependency "bundler" spec.add_development_dependency "rake", "~> 10.0" - spec.add_development_dependency "standard", "0.4.7" + spec.add_development_dependency "standard", "~> 1.4.0" spec.add_development_dependency "activemodel", ">= 4.1" spec.add_development_dependency "graphiti_spec_helpers", "1.0.beta.4" end diff --git a/lib/graphiti.rb b/lib/graphiti.rb index fd3971aa..119f2012 100644 --- a/lib/graphiti.rb +++ b/lib/graphiti.rb @@ -1,5 +1,6 @@ require "json" require "forwardable" +require "uri" require "active_support/core_ext/string" require "active_support/core_ext/enumerable" require "active_support/core_ext/class/attribute" @@ -82,7 +83,7 @@ def self.logger=(val) end def self.log(msg, color = :white, bold = false) - colored = ActiveSupport::LogSubscriber.new.send(:color, msg, color, bold) + colored = ActiveSupport::LogSubscriber.new.send(:color, msg, color, bold: bold) logger.debug(colored) end 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..eb869161 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,38 +41,75 @@ 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) - 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 diff --git a/lib/graphiti/util/serializer_relationships.rb b/lib/graphiti/util/serializer_relationships.rb index 5ab52b2e..70bf886d 100644 --- a/lib/graphiti/util/serializer_relationships.rb +++ b/lib/graphiti/util/serializer_relationships.rb @@ -118,7 +118,7 @@ def validate_link_for_sideload!(sideload) cache_key = :"#{@sideload.object_id}-#{action}" return if self.class.validated_link_cache.include?(cache_key) prc = Graphiti.config.context_for_endpoint - unless prc.call(sideload.resource.endpoint[:full_path], action) + unless prc.call(sideload.resource.endpoint[:full_path].to_s, action) raise Errors::InvalidLink.new(@resource_class, sideload, action) end self.class.validated_link_cache << cache_key diff --git a/lib/graphiti/version.rb b/lib/graphiti/version.rb index 3079d73f..55d1215d 100644 --- a/lib/graphiti/version.rb +++ b/lib/graphiti/version.rb @@ -1,3 +1,3 @@ module Graphiti - VERSION = "1.2.31" + VERSION = "1.2.36" end