feat: invalidate cache after inbox members or team members update (#10869)
At the moment, when updating the inbox members, or team members the account cache used for IndexedDB is not invalidated. This can cause inconsistencies in the UI. This PR fixes this by adding explicit invalidation after performing the member changes ### Summary of changes 1. Added a new method `add_members` and `remove_members` to both `team` and `inbox` models. The change was necessary for two reasons - Since the individual `add_member` and `remove_member` is called in a loop, it's wasteful to run the cache invalidation in the method. - Moving the account cache invalidation call in the controller pollutes the controller business logic 2. Updated tests across the board ### More improvements We can make a concern called `Memberable` with usage like `memberable_with :inbox_members`, that can encapsulate the functionality --- Related: https://github.com/chatwoot/chatwoot/issues/10578
This commit is contained in:
@@ -10,7 +10,7 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
|||||||
def create
|
def create
|
||||||
authorize @inbox, :create?
|
authorize @inbox, :create?
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
agents_to_be_added_ids.map { |user_id| @inbox.add_member(user_id) }
|
@inbox.add_members(agents_to_be_added_ids)
|
||||||
end
|
end
|
||||||
fetch_updated_agents
|
fetch_updated_agents
|
||||||
end
|
end
|
||||||
@@ -24,7 +24,7 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
|||||||
def destroy
|
def destroy
|
||||||
authorize @inbox, :destroy?
|
authorize @inbox, :destroy?
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
params[:user_ids].map { |user_id| @inbox.remove_member(user_id) }
|
@inbox.remove_members(params[:user_ids])
|
||||||
end
|
end
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
@@ -41,8 +41,8 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
|||||||
# the missing ones are the agents which are to be deleted from the inbox
|
# the missing ones are the agents which are to be deleted from the inbox
|
||||||
# the new ones are the agents which are to be added to the inbox
|
# the new ones are the agents which are to be added to the inbox
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
agents_to_be_added_ids.each { |user_id| @inbox.add_member(user_id) }
|
@inbox.add_members(agents_to_be_added_ids)
|
||||||
agents_to_be_removed_ids.each { |user_id| @inbox.remove_member(user_id) }
|
@inbox.remove_members(agents_to_be_removed_ids)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseControll
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@team_members = members_to_be_added_ids.map { |user_id| @team.add_member(user_id) }
|
@team_members = @team.add_members(members_to_be_added_ids)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
members_to_be_added_ids.each { |user_id| @team.add_member(user_id) }
|
@team.add_members(members_to_be_added_ids)
|
||||||
members_to_be_removed_ids.each { |user_id| @team.remove_member(user_id) }
|
@team.remove_members(members_to_be_removed_ids)
|
||||||
end
|
end
|
||||||
@team_members = @team.members
|
@team_members = @team.members
|
||||||
render action: 'create'
|
render action: 'create'
|
||||||
@@ -24,7 +24,7 @@ class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseControll
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
params[:user_ids].map { |user_id| @team.remove_member(user_id) }
|
@team.remove_members(params[:user_ids])
|
||||||
end
|
end
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -82,14 +82,20 @@ class Inbox < ApplicationRecord
|
|||||||
|
|
||||||
scope :order_by_name, -> { order('lower(name) ASC') }
|
scope :order_by_name, -> { order('lower(name) ASC') }
|
||||||
|
|
||||||
def add_member(user_id)
|
# Adds multiple members to the inbox
|
||||||
member = inbox_members.new(user_id: user_id)
|
# @param user_ids [Array<Integer>] Array of user IDs to add as members
|
||||||
member.save!
|
# @return [void]
|
||||||
|
def add_members(user_ids)
|
||||||
|
inbox_members.create!(user_ids.map { |user_id| { user_id: user_id } })
|
||||||
|
update_account_cache
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_member(user_id)
|
# Removes multiple members from the inbox
|
||||||
member = inbox_members.find_by!(user_id: user_id)
|
# @param user_ids [Array<Integer>] Array of user IDs to remove
|
||||||
member.try(:destroy)
|
# @return [void]
|
||||||
|
def remove_members(user_ids)
|
||||||
|
inbox_members.where(user_id: user_ids).destroy_all
|
||||||
|
update_account_cache
|
||||||
end
|
end
|
||||||
|
|
||||||
def facebook?
|
def facebook?
|
||||||
|
|||||||
@@ -31,12 +31,24 @@ class Team < ApplicationRecord
|
|||||||
self.name = name.downcase if attribute_present?('name')
|
self.name = name.downcase if attribute_present?('name')
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_member(user_id)
|
# Adds multiple members to the team
|
||||||
team_members.find_or_create_by(user_id: user_id)&.user
|
# @param user_ids [Array<Integer>] Array of user IDs to add as members
|
||||||
|
# @return [Array<User>] Array of newly added members
|
||||||
|
def add_members(user_ids)
|
||||||
|
team_members_to_create = user_ids.map { |user_id| { user_id: user_id } }
|
||||||
|
created_members = team_members.create(team_members_to_create)
|
||||||
|
added_users = created_members.filter_map(&:user)
|
||||||
|
|
||||||
|
update_account_cache
|
||||||
|
added_users
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_member(user_id)
|
# Removes multiple members from the team
|
||||||
team_members.find_by(user_id: user_id)&.destroy!
|
# @param user_ids [Array<Integer>] Array of user IDs to remove
|
||||||
|
# @return [void]
|
||||||
|
def remove_members(user_ids)
|
||||||
|
team_members.where(user_id: user_ids).destroy_all
|
||||||
|
update_account_cache
|
||||||
end
|
end
|
||||||
|
|
||||||
def messages
|
def messages
|
||||||
|
|||||||
@@ -255,28 +255,30 @@ RSpec.describe 'Inbox Member API', type: :request do
|
|||||||
expect(response).to have_http_status(:not_found)
|
expect(response).to have_http_status(:not_found)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'renders error on invalid params' do
|
it 'ignores invalid params' do
|
||||||
params = { inbox_id: inbox.id, user_ids: ['invalid'] }
|
params = { inbox_id: inbox.id, user_ids: ['invalid'] }
|
||||||
|
original_count = inbox.inbox_members&.count
|
||||||
|
|
||||||
delete "/api/v1/accounts/#{account.id}/inbox_members",
|
delete "/api/v1/accounts/#{account.id}/inbox_members",
|
||||||
headers: administrator.create_new_auth_token,
|
headers: administrator.create_new_auth_token,
|
||||||
params: params,
|
params: params,
|
||||||
as: :json
|
as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:not_found)
|
expect(response).to have_http_status(:success)
|
||||||
expect(response.body).to include('Resource could not be found')
|
expect(inbox.inbox_members&.count).to eq(original_count)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'renders error on non member params' do
|
it 'ignores non member params' do
|
||||||
params = { inbox_id: inbox.id, user_ids: [non_member_agent.id] }
|
params = { inbox_id: inbox.id, user_ids: [non_member_agent.id] }
|
||||||
|
original_count = inbox.inbox_members&.count
|
||||||
|
|
||||||
delete "/api/v1/accounts/#{account.id}/inbox_members",
|
delete "/api/v1/accounts/#{account.id}/inbox_members",
|
||||||
headers: administrator.create_new_auth_token,
|
headers: administrator.create_new_auth_token,
|
||||||
params: params,
|
params: params,
|
||||||
as: :json
|
as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:not_found)
|
expect(response).to have_http_status(:success)
|
||||||
expect(response.body).to include('Resource could not be found')
|
expect(inbox.inbox_members&.count).to eq(original_count)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -41,29 +41,50 @@ RSpec.describe Inbox do
|
|||||||
it_behaves_like 'avatarable'
|
it_behaves_like 'avatarable'
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#add_member' do
|
describe '#add_members' do
|
||||||
let(:inbox) { FactoryBot.create(:inbox) }
|
let(:inbox) { FactoryBot.create(:inbox) }
|
||||||
let(:user) { FactoryBot.create(:user) }
|
|
||||||
|
|
||||||
it do
|
before do
|
||||||
expect(inbox.inbox_members.size).to eq(0)
|
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||||
|
end
|
||||||
|
|
||||||
inbox.add_member(user.id)
|
it 'handles adds all members and resets cache keys' do
|
||||||
expect(inbox.reload.inbox_members.size).to eq(1)
|
users = FactoryBot.create_list(:user, 3)
|
||||||
|
inbox.add_members(users.map(&:id))
|
||||||
|
expect(inbox.reload.inbox_members.size).to eq(3)
|
||||||
|
|
||||||
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch).at_least(:once)
|
||||||
|
.with(
|
||||||
|
'account.cache_invalidated',
|
||||||
|
kind_of(Time),
|
||||||
|
account: inbox.account,
|
||||||
|
cache_keys: inbox.account.cache_keys
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#remove_member' do
|
describe '#remove_members' do
|
||||||
let(:inbox) { FactoryBot.create(:inbox) }
|
let(:inbox) { FactoryBot.create(:inbox) }
|
||||||
let(:user) { FactoryBot.create(:user) }
|
let(:users) { FactoryBot.create_list(:user, 3) }
|
||||||
|
|
||||||
before { inbox.add_member(user.id) }
|
before do
|
||||||
|
inbox.add_members(users.map(&:id))
|
||||||
|
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||||
|
end
|
||||||
|
|
||||||
it do
|
it 'removes the members and resets cache keys' do
|
||||||
expect(inbox.inbox_members.size).to eq(1)
|
expect(inbox.reload.inbox_members.size).to eq(3)
|
||||||
|
|
||||||
inbox.remove_member(user.id)
|
inbox.remove_members(users.map(&:id))
|
||||||
expect(inbox.reload.inbox_members.size).to eq(0)
|
expect(inbox.reload.inbox_members.size).to eq(0)
|
||||||
|
|
||||||
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch).at_least(:once)
|
||||||
|
.with(
|
||||||
|
'account.cache_invalidated',
|
||||||
|
kind_of(Time),
|
||||||
|
account: inbox.account,
|
||||||
|
cache_keys: inbox.account.cache_keys
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,51 @@ RSpec.describe Team do
|
|||||||
it { is_expected.to have_many(:conversations) }
|
it { is_expected.to have_many(:conversations) }
|
||||||
it { is_expected.to have_many(:team_members) }
|
it { is_expected.to have_many(:team_members) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#add_members' do
|
||||||
|
let(:team) { FactoryBot.create(:team) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles adds all members and resets cache keys' do
|
||||||
|
users = FactoryBot.create_list(:user, 3)
|
||||||
|
team.add_members(users.map(&:id))
|
||||||
|
expect(team.reload.team_members.size).to eq(3)
|
||||||
|
|
||||||
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch).at_least(:once)
|
||||||
|
.with(
|
||||||
|
'account.cache_invalidated',
|
||||||
|
kind_of(Time),
|
||||||
|
account: team.account,
|
||||||
|
cache_keys: team.account.cache_keys
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#remove_members' do
|
||||||
|
let(:team) { FactoryBot.create(:team) }
|
||||||
|
let(:users) { FactoryBot.create_list(:user, 3) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
team.add_members(users.map(&:id))
|
||||||
|
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes the members and resets cache keys' do
|
||||||
|
expect(team.reload.team_members.size).to eq(3)
|
||||||
|
|
||||||
|
team.remove_members(users.map(&:id))
|
||||||
|
expect(team.reload.team_members.size).to eq(0)
|
||||||
|
|
||||||
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch).at_least(:once)
|
||||||
|
.with(
|
||||||
|
'account.cache_invalidated',
|
||||||
|
kind_of(Time),
|
||||||
|
account: team.account,
|
||||||
|
cache_keys: team.account.cache_keys
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user