fix: Notification page breakages (#5236)

- Remove the cascading foreign key indexes
- Add migration to clean up existing objects

fixes: #4285
This commit is contained in:
Sojan Jose 2022-08-10 13:46:46 +02:00 committed by GitHub
parent 12b6fb211a
commit 74fdfffe08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 93 additions and 115 deletions

View file

@ -183,3 +183,4 @@ AllCops:
- db/migrate/20200503151130_add_account_feature_flag.rb
- db/migrate/20200927135222_add_last_activity_at_to_conversation.rb
- db/migrate/20210306170117_add_last_activity_at_to_contacts.rb
- db/migrate/20220809104508_revert_cascading_indexes.rb

View file

@ -0,0 +1,19 @@
# Delete migration and spec after 2 consecutive releases.
class Migration::RemoveStaleNotificationsJob < ApplicationJob
queue_as :scheduled_jobs
def perform
remove_invalid_messages
end
private
def remove_invalid_messages
deleted_ids = []
Message.distinct.pluck(:inbox_id).each_slice(1000) do |id_list|
deleted_ids << (id_list - Inbox.where(id: id_list).pluck(:id))
end
Message.where(inbox_id: deleted_ids.flatten).destroy_all
end
end

View file

@ -55,7 +55,7 @@ class Account < ApplicationRecord
has_many :csat_survey_responses, dependent: :destroy_async
has_many :custom_attribute_definitions, dependent: :destroy_async
has_many :custom_filters, dependent: :destroy_async
has_many :dashboard_apps, dependent: :destroy
has_many :dashboard_apps, dependent: :destroy_async
has_many :data_imports, dependent: :destroy_async
has_many :email_channels, dependent: :destroy_async, class_name: '::Channel::Email'
has_many :facebook_pages, dependent: :destroy_async, class_name: '::Channel::FacebookPage'
@ -67,7 +67,7 @@ class Account < ApplicationRecord
has_many :messages, dependent: :destroy_async
has_many :notes, dependent: :destroy_async
has_many :notification_settings, dependent: :destroy_async
has_many :notifications, dependent: :destroy
has_many :notifications, dependent: :destroy_async
has_many :portals, dependent: :destroy_async, class_name: '::Portal'
has_many :sms_channels, dependent: :destroy_async, class_name: '::Channel::Sms'
has_many :teams, dependent: :destroy_async

View file

@ -19,11 +19,6 @@
# index_account_users_on_user_id (user_id)
# uniq_user_id_per_account_id (account_id,user_id) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id) ON DELETE => cascade
# fk_rails_... (user_id => users.id) ON DELETE => cascade
#
class AccountUser < ApplicationRecord
include AvailabilityStatusable

View file

@ -16,10 +16,6 @@
#
# index_agent_bots_on_account_id (account_id)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id) ON DELETE => cascade
#
class AgentBot < ApplicationRecord
include AccessTokenable

View file

@ -23,11 +23,6 @@
# index_articles_on_associated_article_id (associated_article_id)
# index_articles_on_author_id (author_id)
#
# Foreign Keys
#
# fk_rails_... (associated_article_id => articles.id)
# fk_rails_... (author_id => users.id)
#
class Article < ApplicationRecord
include PgSearch::Model

View file

@ -28,11 +28,6 @@
# index_campaigns_on_inbox_id (inbox_id)
# index_campaigns_on_scheduled_at (scheduled_at)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id) ON DELETE => cascade
# fk_rails_... (inbox_id => inboxes.id) ON DELETE => cascade
#
class Campaign < ApplicationRecord
include UrlHelper
validates :account_id, presence: true

View file

@ -23,11 +23,6 @@
# index_categories_on_parent_category_id (parent_category_id)
# index_categories_on_slug_and_locale_and_portal_id (slug,locale,portal_id) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (associated_category_id => categories.id)
# fk_rails_... (parent_category_id => categories.id)
#
class Category < ApplicationRecord
belongs_to :account
belongs_to :portal

View file

@ -19,11 +19,6 @@
# index_contact_inboxes_on_pubsub_token (pubsub_token) UNIQUE
# index_contact_inboxes_on_source_id (source_id)
#
# Foreign Keys
#
# fk_rails_... (contact_id => contacts.id) ON DELETE => cascade
# fk_rails_... (inbox_id => inboxes.id) ON DELETE => cascade
#
class ContactInbox < ApplicationRecord
include Pubsubable

View file

@ -37,12 +37,6 @@
# index_conversations_on_status_and_account_id (status,account_id)
# index_conversations_on_team_id (team_id)
#
# Foreign Keys
#
# fk_rails_... (campaign_id => campaigns.id) ON DELETE => cascade
# fk_rails_... (contact_inbox_id => contact_inboxes.id) ON DELETE => cascade
# fk_rails_... (team_id => teams.id) ON DELETE => cascade
#
class Conversation < ApplicationRecord
include Labelable
@ -88,7 +82,7 @@ class Conversation < ApplicationRecord
has_many :mentions, dependent: :destroy_async
has_many :messages, dependent: :destroy_async, autosave: true
has_one :csat_survey_response, dependent: :destroy_async
has_many :notifications, as: :primary_actor, dependent: :destroy
has_many :notifications, as: :primary_actor, dependent: :destroy_async
before_save :ensure_snooze_until_reset
before_create :mark_conversation_pending_if_bot

View file

@ -21,14 +21,6 @@
# index_csat_survey_responses_on_conversation_id (conversation_id)
# index_csat_survey_responses_on_message_id (message_id) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id) ON DELETE => cascade
# fk_rails_... (assigned_agent_id => users.id) ON DELETE => cascade
# fk_rails_... (contact_id => contacts.id) ON DELETE => cascade
# fk_rails_... (conversation_id => conversations.id) ON DELETE => cascade
# fk_rails_... (message_id => messages.id) ON DELETE => cascade
#
class CsatSurveyResponse < ApplicationRecord
belongs_to :account
belongs_to :conversation

View file

@ -15,11 +15,6 @@
# index_dashboard_apps_on_account_id (account_id)
# index_dashboard_apps_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (user_id => users.id)
#
class DashboardApp < ApplicationRecord
belongs_to :user
belongs_to :account

View file

@ -16,10 +16,6 @@
#
# index_data_imports_on_account_id (account_id)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id) ON DELETE => cascade
#
class DataImport < ApplicationRecord
belongs_to :account
validates :data_type, inclusion: { in: ['contacts'], message: I18n.t('errors.data_import.data_type.invalid') }

View file

@ -18,11 +18,6 @@
# index_macros_on_created_by_id (created_by_id)
# index_macros_on_updated_by_id (updated_by_id)
#
# Foreign Keys
#
# fk_rails_... (created_by_id => users.id)
# fk_rails_... (updated_by_id => users.id)
#
class Macro < ApplicationRecord
belongs_to :account
belongs_to :created_by,

View file

@ -17,11 +17,6 @@
# index_mentions_on_user_id (user_id)
# index_mentions_on_user_id_and_conversation_id (user_id,conversation_id) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (conversation_id => conversations.id) ON DELETE => cascade
# fk_rails_... (user_id => users.id) ON DELETE => cascade
#
class Mention < ApplicationRecord
include SortHandler

View file

@ -85,8 +85,9 @@ class Message < ApplicationRecord
belongs_to :contact, required: false
belongs_to :sender, polymorphic: true, required: false
has_many :attachments, dependent: :destroy_async, autosave: true, before_add: :validate_attachments_limit
has_many :attachments, dependent: :destroy, autosave: true, before_add: :validate_attachments_limit
has_one :csat_survey_response, dependent: :destroy_async
has_many :notifications, as: :primary_actor, dependent: :destroy_async
after_create_commit :execute_after_create_commit_callbacks

View file

@ -16,12 +16,6 @@
# index_notes_on_contact_id (contact_id)
# index_notes_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id) ON DELETE => cascade
# fk_rails_... (contact_id => contacts.id) ON DELETE => cascade
# fk_rails_... (user_id => users.id) ON DELETE => cascade
#
class Note < ApplicationRecord
before_validation :ensure_account_id
validates :content, presence: true

View file

@ -15,10 +15,6 @@
# index_teams_on_account_id (account_id)
# index_teams_on_name_and_account_id (name,account_id) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id) ON DELETE => cascade
#
class Team < ApplicationRecord
belongs_to :account
has_many :team_members, dependent: :destroy_async

View file

@ -14,11 +14,6 @@
# index_team_members_on_team_id_and_user_id (team_id,user_id) UNIQUE
# index_team_members_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (team_id => teams.id) ON DELETE => cascade
# fk_rails_... (user_id => users.id) ON DELETE => cascade
#
class TeamMember < ApplicationRecord
belongs_to :user
belongs_to :team

View file

@ -0,0 +1,46 @@
class RevertCascadingIndexes < ActiveRecord::Migration[6.1]
def change
remove_foreign_key 'account_users', 'accounts' if foreign_key_exists? 'account_users', 'accounts'
remove_foreign_key 'account_users', 'users' if foreign_key_exists? 'account_users', 'users'
remove_foreign_key 'agent_bots', 'accounts' if foreign_key_exists? 'agent_bots', 'accounts'
remove_foreign_key 'campaigns', 'accounts' if foreign_key_exists? 'campaigns', 'accounts'
remove_foreign_key 'campaigns', 'inboxes' if foreign_key_exists? 'campaigns', 'inboxes'
remove_foreign_key 'conversations', 'campaigns' if foreign_key_exists? 'conversations', 'campaigns'
remove_foreign_key 'conversations', 'contact_inboxes' if foreign_key_exists? 'conversations', 'contact_inboxes'
remove_foreign_key 'conversations', 'teams' if foreign_key_exists? 'conversations', 'teams'
remove_foreign_key 'csat_survey_responses', 'accounts' if foreign_key_exists? 'csat_survey_responses', 'accounts'
remove_foreign_key 'csat_survey_responses', 'contacts' if foreign_key_exists? 'csat_survey_responses', 'contacts'
remove_foreign_key 'csat_survey_responses', 'conversations' if foreign_key_exists? 'csat_survey_responses', 'conversations'
remove_foreign_key 'csat_survey_responses', 'messages' if foreign_key_exists? 'csat_survey_responses', 'messages'
remove_foreign_key 'csat_survey_responses', 'users', column: 'assigned_agent_id' if foreign_key_exists? 'csat_survey_responses', 'users',
column: 'assigned_agent_id'
remove_foreign_key 'data_imports', 'accounts' if foreign_key_exists? 'data_imports', 'accounts'
remove_foreign_key 'mentions', 'conversations' if foreign_key_exists? 'mentions', 'conversations'
remove_foreign_key 'mentions', 'users' if foreign_key_exists? 'mentions', 'users'
remove_foreign_key 'notes', 'accounts' if foreign_key_exists? 'notes', 'accounts'
remove_foreign_key 'notes', 'contacts' if foreign_key_exists? 'notes', 'contacts'
remove_foreign_key 'notes', 'users' if foreign_key_exists? 'notes', 'users'
remove_foreign_key 'team_members', 'teams' if foreign_key_exists? 'team_members', 'teams'
remove_foreign_key 'team_members', 'users' if foreign_key_exists? 'team_members', 'users'
remove_foreign_key 'teams', 'accounts' if foreign_key_exists? 'teams', 'accounts'
remove_foreign_key 'contact_inboxes', 'contacts' if foreign_key_exists? 'contact_inboxes', 'contacts'
remove_foreign_key 'contact_inboxes', 'inboxes' if foreign_key_exists? 'contact_inboxes', 'inboxes'
remove_foreign_key 'articles', 'articles', column: 'associated_article_id' if foreign_key_exists? 'articles', 'articles',
column: 'associated_article_id'
remove_foreign_key 'articles', 'users', column: 'author_id' if foreign_key_exists? 'articles', 'users', column: 'author_id'
remove_foreign_key 'categories', 'categories', column: 'parent_category_id' if foreign_key_exists? 'categories', 'categories',
column: 'parent_category_id'
remove_foreign_key 'categories', 'categories', column: 'associated_category_id' if foreign_key_exists? 'categories', 'categories',
column: 'associated_category_id'
remove_foreign_key 'dashboard_apps', 'accounts' if foreign_key_exists? 'dashboard_apps', 'accounts'
remove_foreign_key 'dashboard_apps', 'users' if foreign_key_exists? 'dashboard_apps', 'users'
remove_foreign_key 'macros', 'users', column: 'created_by_id' if foreign_key_exists? 'macros', 'users', column: 'created_by_id'
remove_foreign_key 'macros', 'users', column: 'updated_by_id' if foreign_key_exists? 'macros', 'users', column: 'updated_by_id'
Migration::RemoveStaleNotificationsJob.perform_later
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_08_02_133722) do
ActiveRecord::Schema.define(version: 2022_08_09_104508) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
@ -862,40 +862,8 @@ ActiveRecord::Schema.define(version: 2022_08_02_133722) do
t.index ["inbox_id"], name: "index_working_hours_on_inbox_id"
end
add_foreign_key "account_users", "accounts", on_delete: :cascade
add_foreign_key "account_users", "users", on_delete: :cascade
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "agent_bots", "accounts", on_delete: :cascade
add_foreign_key "articles", "articles", column: "associated_article_id"
add_foreign_key "articles", "users", column: "author_id"
add_foreign_key "campaigns", "accounts", on_delete: :cascade
add_foreign_key "campaigns", "inboxes", on_delete: :cascade
add_foreign_key "categories", "categories", column: "associated_category_id"
add_foreign_key "categories", "categories", column: "parent_category_id"
add_foreign_key "contact_inboxes", "contacts", on_delete: :cascade
add_foreign_key "contact_inboxes", "inboxes", on_delete: :cascade
add_foreign_key "conversations", "campaigns", on_delete: :cascade
add_foreign_key "conversations", "contact_inboxes", on_delete: :cascade
add_foreign_key "conversations", "teams", on_delete: :cascade
add_foreign_key "csat_survey_responses", "accounts", on_delete: :cascade
add_foreign_key "csat_survey_responses", "contacts", on_delete: :cascade
add_foreign_key "csat_survey_responses", "conversations", on_delete: :cascade
add_foreign_key "csat_survey_responses", "messages", on_delete: :cascade
add_foreign_key "csat_survey_responses", "users", column: "assigned_agent_id", on_delete: :cascade
add_foreign_key "dashboard_apps", "accounts"
add_foreign_key "dashboard_apps", "users"
add_foreign_key "data_imports", "accounts", on_delete: :cascade
add_foreign_key "macros", "users", column: "created_by_id"
add_foreign_key "macros", "users", column: "updated_by_id"
add_foreign_key "mentions", "conversations", on_delete: :cascade
add_foreign_key "mentions", "users", on_delete: :cascade
add_foreign_key "notes", "accounts", on_delete: :cascade
add_foreign_key "notes", "contacts", on_delete: :cascade
add_foreign_key "notes", "users", on_delete: :cascade
add_foreign_key "team_members", "teams", on_delete: :cascade
add_foreign_key "team_members", "users", on_delete: :cascade
add_foreign_key "teams", "accounts", on_delete: :cascade
create_trigger("accounts_after_insert_row_tr", :generated => true, :compatibility => 1).
on("accounts").
after(:insert).

View file

@ -581,12 +581,17 @@ RSpec.describe Conversation, type: :model do
end
describe '#delete conversation' do
include ActiveJob::TestHelper
let!(:conversation) { create(:conversation) }
let!(:notification) { create(:notification, notification_type: 'conversation_creation', primary_actor: conversation) }
it 'delete associated notifications if conversation is deleted' do
conversation.destroy!
perform_enqueued_jobs do
conversation.destroy!
end
expect { notification.reload }.to raise_error ActiveRecord::RecordNotFound
end
end

View file

@ -3,6 +3,8 @@
require 'rails_helper'
RSpec.describe Notification do
include ActiveJob::TestHelper
context 'with associations' do
it { is_expected.to belong_to(:account) }
it { is_expected.to belong_to(:user) }
@ -97,4 +99,17 @@ RSpec.describe Notification do
})
end
end
context 'when primary actory is deleted' do
let!(:conversation) { create(:conversation) }
it 'clears notifications' do
notification = create(:notification, notification_type: 'conversation_creation', primary_actor: conversation)
perform_enqueued_jobs do
conversation.inbox.destroy!
end
expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end