mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	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:
		| @@ -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 | ||||
|   | ||||
							
								
								
									
										19
									
								
								app/jobs/migration/remove_stale_notifications_job.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/jobs/migration/remove_stale_notifications_job.rb
									
									
									
									
									
										Normal 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 | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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') } | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										46
									
								
								db/migrate/20220809104508_revert_cascading_indexes.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								db/migrate/20220809104508_revert_cascading_indexes.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										34
									
								
								db/schema.rb
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								db/schema.rb
									
									
									
									
									
								
							| @@ -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). | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Sojan Jose
					Sojan Jose