Skip to content

Commit cc74080

Browse files
committed
feat: Invalidate elements cache if related object changes
Add a after_touch callback to RelatableResurce that enqueues a job that invalidates the element and page caches for ingredients having this object assigned as related_object. So, when a page or node changes the elements and pages get touched as well. Taken from alchemy-solidus extension, but actually useful for all related objects.
1 parent 37a0900 commit cc74080

4 files changed

Lines changed: 111 additions & 0 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module Alchemy
2+
class InvalidateElementsCacheJob < BaseJob
3+
queue_as :default
4+
5+
def perform(related_object_type, id)
6+
element_ids = Ingredient
7+
.where(related_object_id: id, related_object_type:)
8+
.joins(:element)
9+
.pluck(:element_id)
10+
elements = Element.where(id: element_ids)
11+
12+
all_element_ids = get_all_element_ids(elements, element_ids)
13+
Element.where(id: all_element_ids).touch_all
14+
15+
page_ids = elements.joins(page_version: :page).select("DISTINCT alchemy_pages.id")
16+
Page.where(id: page_ids).touch_all
17+
end
18+
19+
private
20+
21+
def get_all_element_ids(elements, element_ids)
22+
parent_element_ids = elements.where.not(parent_element_id: nil).pluck(:parent_element_id)
23+
parent_elements = Element.distinct.where(id: parent_element_ids)
24+
25+
if parent_elements.any?
26+
element_ids += parent_element_ids
27+
get_all_element_ids(parent_elements, element_ids)
28+
else
29+
element_ids
30+
end
31+
end
32+
end
33+
end

app/models/concerns/alchemy/relatable_resource.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,20 @@ def alchemy_element_preloads(records)
4141

4242
has_many :related_elements, through: :related_ingredients, source: :element
4343
has_many :related_pages, through: :related_elements, source: :page
44+
45+
after_touch :touch_related_ingredients, if: -> { related_ingredients.exists? }
4446
end
4547

4648
# Returns true if object is not assigned to any ingredient.
4749
#
4850
def deletable?
4951
related_ingredients.none?
5052
end
53+
54+
private
55+
56+
def touch_related_ingredients
57+
InvalidateElementsCacheJob.perform_later(self.class.name, id)
58+
end
5159
end
5260
end

lib/alchemy/test_support/relatable_resource_examples.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,24 @@
5555
it { is_expected.to be(true) }
5656
end
5757
end
58+
59+
describe "after_touch" do
60+
let(:related_object) { create(:"alchemy_#{args[:resource_name]}") }
61+
62+
context "when related ingredients exist" do
63+
let!(:ingredient) { create(:"alchemy_ingredient_#{args[:ingredient_type]}", related_object:) }
64+
65+
it "enqueues InvalidateElementsCacheJob" do
66+
expect {
67+
related_object.touch
68+
}.to have_enqueued_job(Alchemy::InvalidateElementsCacheJob).with(described_class.name, related_object.id)
69+
end
70+
end
71+
72+
context "when no related ingredients exist" do
73+
it "does not enqueue InvalidateElementsCacheJob" do
74+
expect { related_object.touch }.to_not have_enqueued_job(Alchemy::InvalidateElementsCacheJob)
75+
end
76+
end
77+
end
5878
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
require "rails_helper"
2+
3+
RSpec.describe Alchemy::InvalidateElementsCacheJob, type: :job do
4+
let(:job) { described_class.new }
5+
6+
subject { job.perform("Alchemy::Page", related_object.id) }
7+
8+
let(:page) { create(:alchemy_page, :public, updated_at: 1.week.ago) }
9+
10+
let!(:element) do
11+
create(:alchemy_element, page_version: page.draft_version, ingredients: [ingredient], updated_at: 1.day.ago)
12+
end
13+
14+
describe "#perform" do
15+
let(:related_object) { create(:alchemy_page) }
16+
let(:ingredient) { Alchemy::Ingredients::Page.create(related_object:, role: "page") }
17+
18+
it "touches the element" do
19+
expect { subject }.to change { element.reload.updated_at }
20+
end
21+
22+
context "when the element has a parent" do
23+
let!(:parent_element) do
24+
create(:alchemy_element, page_version: page.draft_version, updated_at: 1.day.ago).tap do |parent|
25+
element.update_column(:parent_element_id, parent.id)
26+
end
27+
end
28+
29+
it "touches the parent element" do
30+
expect { subject }.to change { parent_element.reload.updated_at }
31+
end
32+
33+
context "that has a parent" do
34+
let!(:grand_parent_element) do
35+
create(:alchemy_element, page_version: page.draft_version, updated_at: 1.day.ago).tap do |grand_parent|
36+
parent_element.update_column(:parent_element_id, grand_parent.id)
37+
end
38+
end
39+
40+
it "touches the grand parent element" do
41+
expect { subject }.to change { grand_parent_element.reload.updated_at }
42+
end
43+
end
44+
end
45+
46+
it "touches the page" do
47+
expect { subject }.to change { page.reload.updated_at }
48+
end
49+
end
50+
end

0 commit comments

Comments
 (0)