diff --git a/app/models/alchemy/page/publisher.rb b/app/models/alchemy/page/publisher.rb index fcdc97802b..4c6c941740 100644 --- a/app/models/alchemy/page/publisher.rb +++ b/app/models/alchemy/page/publisher.rb @@ -17,7 +17,7 @@ def initialize(page) def publish!(public_on:) Page.transaction do version = public_version(public_on) - version.elements.not_nested.destroy_all + DeleteElements.new(version.elements).call # We must not use .find_each here to not mess up the order of elements page.draft_version.elements.not_nested.available.each do |element| diff --git a/app/services/alchemy/delete_elements.rb b/app/services/alchemy/delete_elements.rb new file mode 100644 index 0000000000..3870b875c4 --- /dev/null +++ b/app/services/alchemy/delete_elements.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Alchemy + class DeleteElements + class WouldLeaveOrphansError < StandardError; end + attr_reader :elements + + def initialize(elements) + @elements = elements + end + + def call + if orphanable_records.present? + raise WouldLeaveOrphansError + end + + contents = Alchemy::Content.where(element_id: elements.map(&:id)) + contents.group_by(&:essence_type) + .transform_values! { |value| value.map(&:essence_id) } + .each do |class_name, ids| + class_name.constantize.where(id: ids).delete_all + end + contents.delete_all + delete_elements + end + + private + + def orphanable_records + Alchemy::Element.where(parent_element_id: [elements]).where.not(id: elements) + end + + def delete_elements + case elements + when ActiveRecord::Associations::CollectionProxy + elements.delete_all(:delete_all) + when ActiveRecord::Relation + elements.delete_all + else + Alchemy::Element.where(id: elements).delete_all + end + end + end +end diff --git a/spec/services/alchemy/delete_elements_spec.rb b/spec/services/alchemy/delete_elements_spec.rb new file mode 100644 index 0000000000..2e2ea95348 --- /dev/null +++ b/spec/services/alchemy/delete_elements_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Alchemy::DeleteElements do + let!(:parent_element) { create(:alchemy_element, :with_nestable_elements, :with_contents) } + let!(:nested_element) { parent_element.nested_elements.first } + let!(:normal_element) { create(:alchemy_element, :with_contents) } + + before do + expect(Alchemy::Element.count).not_to be_zero + expect(Alchemy::Content.count).not_to be_zero + expect(Alchemy::EssenceText.count).not_to be_zero + expect(Alchemy::EssencePicture.count).not_to be_zero + expect(Alchemy::EssenceRichtext.count).not_to be_zero + end + + subject { Alchemy::DeleteElements.new(elements).call } + + context "with all elements" do + let(:elements) { [parent_element, nested_element, normal_element] } + + it "destroys all elements" do + subject + expect(Alchemy::Element.count).to be_zero + expect(Alchemy::Content.count).to be_zero + expect(Alchemy::EssenceText.count).to be_zero + expect(Alchemy::EssencePicture.count).to be_zero + expect(Alchemy::EssenceRichtext.count).to be_zero + end + + context "when calling with an ActiveRecord::Relation" do + let(:elements) { Alchemy::Element.all } + + it "works" do + subject + expect(Alchemy::Element.count).to be_zero + expect(Alchemy::Content.count).to be_zero + expect(Alchemy::EssenceText.count).to be_zero + expect(Alchemy::EssencePicture.count).to be_zero + expect(Alchemy::EssenceRichtext.count).to be_zero + end + end + + context "when calling it as an association" do + let(:page_version) { create(:alchemy_page_version) } + let(:elements) { page_version.elements } + before do + Alchemy::Element.update_all(page_version_id: page_version.id) + end + + it "works" do + subject + expect(Alchemy::Element.count).to be_zero + expect(Alchemy::Content.count).to be_zero + expect(Alchemy::EssenceText.count).to be_zero + expect(Alchemy::EssencePicture.count).to be_zero + expect(Alchemy::EssenceRichtext.count).to be_zero + end + end + end + + context "when calling with an element having nested elements that is not in the collection" do + let(:elements) { [parent_element, normal_element] } + + it "raises an error and deletes nothing" do + expect { subject }.to raise_exception(Alchemy::DeleteElements::WouldLeaveOrphansError) + end + end +end