Purge leftover information in my channel table when purging playbooks (#9057)

* Purge leftover information in my channel table when purging playbooks

* Add comment

* Clear the sync ephemeral store on plugin disable
This commit is contained in:
Daniel Espino García 2025-08-12 14:27:52 +02:00 committed by GitHub
parent d653d55cbc
commit 71cec5a3e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 50 additions and 3 deletions

View file

@ -4,15 +4,29 @@
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
import {getMyChannel} from '@queries/servers/channel';
import {querySystemValue} from '@queries/servers/system';
import TestHelper from '@test/test_helper';
import {updateLastPlaybookRunsFetchAt} from './channel';
import {setPlaybooksVersion} from './version';
import type ServerDataOperator from '@database/operator/server_data_operator';
const serverUrl = 'baseHandler.test.com';
const channelId = 'channelid';
let operator: ServerDataOperator;
const channel: Channel = TestHelper.fakeChannel({
id: channelId,
team_id: 'teamid1',
});
const channelMember: ChannelMembership = TestHelper.fakeChannelMember({
id: 'id',
channel_id: channelId,
});
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
@ -50,6 +64,11 @@ describe('setPlaybooksVersion', () => {
it('should purge playbooks when version is empty', async () => {
const database = operator.database;
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
await operator.handleMyChannel({channels: [channel], myChannels: [channelMember], prepareRecordsOnly: false});
await updateLastPlaybookRunsFetchAt(serverUrl, channelId, 1234);
jest.spyOn(database.adapter, 'unsafeExecute').mockImplementation(() => {
return Promise.resolve();
});
@ -65,10 +84,18 @@ describe('setPlaybooksVersion', () => {
[`DELETE FROM ${PLAYBOOK_TABLES.PLAYBOOK_CHECKLIST_ITEM}`, []],
],
});
const myChannel = await getMyChannel(operator.database, channelId);
expect(myChannel?.lastPlaybookRunsFetchAt).toBe(0);
});
it('should not purge playbooks when version is not empty', async () => {
const database = operator.database;
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
await operator.handleMyChannel({channels: [channel], myChannels: [channelMember], prepareRecordsOnly: false});
await updateLastPlaybookRunsFetchAt(serverUrl, channelId, 1234);
jest.spyOn(database.adapter, 'unsafeExecute').mockImplementation(() => {
return Promise.resolve();
});
@ -78,6 +105,8 @@ describe('setPlaybooksVersion', () => {
expect(data).toBe(true);
expect(database.adapter.unsafeExecute).not.toHaveBeenCalled();
const myChannel = await getMyChannel(operator.database, channelId);
expect(myChannel?.lastPlaybookRunsFetchAt).toBe(1234);
});
it('should handle purge playbooks errors', async () => {

View file

@ -1,9 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import {Q} from '@nozbe/watermelondb';
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
import EphemeralStore from '@store/ephemeral_store';
import type {MyChannelModel} from '@database/models/server';
export const setPlaybooksVersion = async (serverUrl: string, version: string) => {
try {
@ -21,6 +26,8 @@ export const setPlaybooksVersion = async (serverUrl: string, version: string) =>
if (error) {
return {error};
}
EphemeralStore.clearChannelPlaybooksSynced(serverUrl);
}
return {data: true};
@ -32,14 +39,25 @@ export const setPlaybooksVersion = async (serverUrl: string, version: string) =>
const purgePlaybooks = async (serverUrl: string) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
await database.write(() => {
return database.adapter.unsafeExecute({
const models = await database.collections.get<MyChannelModel>(MM_TABLES.SERVER.MY_CHANNEL).query(Q.where('last_playbook_runs_fetch_at', Q.gt(0)));
await database.write(async () => {
await database.adapter.unsafeExecute({
sqls: [
[`DELETE FROM ${PLAYBOOK_TABLES.PLAYBOOK_RUN}`, []],
[`DELETE FROM ${PLAYBOOK_TABLES.PLAYBOOK_CHECKLIST}`, []],
[`DELETE FROM ${PLAYBOOK_TABLES.PLAYBOOK_CHECKLIST_ITEM}`, []],
],
});
// Process each model sequentially to prevent overwhelming the event loop with excessive promises
// and to avoid overloading the database with concurrent write operations, especially when handling a large number of channels.
for await (const model of models) {
await model.update((channel) => {
channel.lastPlaybookRunsFetchAt = 0;
return channel;
});
}
});
} catch (error) {
return {error};