From 980c31f40fafee2032d81df466ea661b36234083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Fri, 3 Feb 2023 16:11:57 +0100 Subject: [PATCH] Remove watermelondb limitation on updating an already updated model (#7067) * Remove watermelondb limitation on updating an already updated model * Add logic to handle different prepare states and improve logging * fix tests --------- Co-authored-by: Elias Nahum --- app/actions/local/category.ts | 4 +- app/actions/local/channel.ts | 24 +- app/actions/local/draft.ts | 8 +- app/actions/local/post.ts | 8 +- app/actions/local/team.ts | 4 +- app/actions/local/thread.ts | 18 +- app/actions/local/user.ts | 4 +- app/actions/remote/channel.ts | 22 +- app/actions/remote/entry/app.ts | 6 +- app/actions/remote/entry/common.ts | 2 +- app/actions/remote/entry/gql_common.ts | 2 +- app/actions/remote/entry/login.ts | 2 +- app/actions/remote/entry/notification.ts | 2 +- app/actions/remote/file.ts | 4 +- app/actions/remote/groups.ts | 6 +- app/actions/remote/post.ts | 30 +- app/actions/remote/reactions.ts | 2 +- app/actions/remote/retry.ts | 4 +- app/actions/remote/search.ts | 2 +- app/actions/remote/team.ts | 10 +- app/actions/remote/terms_of_service.ts | 2 +- app/actions/remote/thread.ts | 4 +- app/actions/remote/user.ts | 14 +- app/actions/websocket/category.ts | 2 +- app/actions/websocket/channel.ts | 14 +- app/actions/websocket/index.ts | 2 +- app/actions/websocket/posts.ts | 6 +- app/actions/websocket/roles.ts | 4 +- app/actions/websocket/teams.ts | 2 +- app/actions/websocket/users.ts | 2 +- app/client/rest/apps.ts | 6 +- app/client/rest/base.ts | 7 +- app/client/rest/categories.ts | 4 +- app/client/rest/channels.ts | 30 +- app/client/rest/emojis.ts | 4 +- app/client/rest/files.ts | 9 +- app/client/rest/general.ts | 6 +- app/client/rest/groups.ts | 4 +- app/client/rest/integrations.ts | 10 +- app/client/rest/nps.ts | 4 +- app/client/rest/plugins.ts | 4 +- app/client/rest/posts.ts | 40 +- app/client/rest/preferences.ts | 8 +- app/client/rest/teams.ts | 22 +- app/client/rest/threads.ts | 4 +- app/client/rest/tos.ts | 4 +- app/client/rest/users.ts | 46 +-- .../operator/app_data_operator/index.test.ts | 4 +- .../operator/app_data_operator/index.ts | 4 +- .../operator/base_data_operator/index.ts | 34 +- .../handlers/category.test.ts | 4 +- .../server_data_operator/handlers/category.ts | 9 +- .../handlers/channel.test.ts | 10 +- .../server_data_operator/handlers/channel.ts | 13 +- .../handlers/group.test.ts | 2 +- .../server_data_operator/handlers/group.ts | 23 +- .../handlers/index.test.ts | 10 +- .../server_data_operator/handlers/index.ts | 77 +++- .../handlers/post.test.ts | 2 +- .../server_data_operator/handlers/post.ts | 367 +++++++++++++++++- .../handlers/posts_in_channel.ts | 298 -------------- .../handlers/posts_in_thread.ts | 77 ---- .../server_data_operator/handlers/reaction.ts | 78 ---- .../handlers/team.test.ts | 10 +- .../server_data_operator/handlers/team.ts | 13 +- .../handlers/team_threads_sync.ts | 5 +- .../handlers/thread.test.ts | 2 +- .../server_data_operator/handlers/thread.ts | 67 +++- .../handlers/thread_in_team.ts | 69 ---- .../handlers/user.test.ts | 4 +- .../server_data_operator/handlers/user.ts | 9 +- .../operator/server_data_operator/index.ts | 12 - app/database/operator/utils/general.ts | 5 +- .../draft_upload_manager/index.test.ts | 36 +- app/queries/servers/channel.ts | 2 +- app/queries/servers/preference.ts | 2 +- app/queries/servers/system.ts | 4 +- app/queries/servers/team.ts | 2 +- app/utils/mix.ts | 10 +- patches/@nozbe+watermelondb+0.25.5.patch | 51 ++- test/test_helper.ts | 14 +- types/database/database.ts | 14 +- types/utils/mixins.d.ts | 3 + 83 files changed, 893 insertions(+), 881 deletions(-) delete mode 100644 app/database/operator/server_data_operator/handlers/posts_in_channel.ts delete mode 100644 app/database/operator/server_data_operator/handlers/posts_in_thread.ts delete mode 100644 app/database/operator/server_data_operator/handlers/reaction.ts delete mode 100644 app/database/operator/server_data_operator/handlers/thread_in_team.ts create mode 100644 types/utils/mixins.d.ts diff --git a/app/actions/local/category.ts b/app/actions/local/category.ts index 73f19eace..f1f9eee7e 100644 --- a/app/actions/local/category.ts +++ b/app/actions/local/category.ts @@ -39,7 +39,7 @@ export async function storeCategories(serverUrl: string, categories: CategoryWit } if (models.length > 0) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'storeCategories'); } return {models}; @@ -103,7 +103,7 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch const models = await prepareCategoryChannels(operator, categoriesWithChannels); if (models.length && !prepareRecordsOnly) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'addChannelToDefaultCategory'); } return {models}; diff --git a/app/actions/local/channel.ts b/app/actions/local/channel.ts index 6ab57bb6a..880c3a0c0 100644 --- a/app/actions/local/channel.ts +++ b/app/actions/local/channel.ts @@ -82,7 +82,7 @@ export async function switchToChannel(serverUrl: string, channelId: string, team } if (models.length && !prepareRecordsOnly) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'switchToChannel'); } if (isTabletDevice) { @@ -124,7 +124,7 @@ export async function removeCurrentUserFromChannel(serverUrl: string, channelId: await removeChannelFromTeamHistory(operator, teamId, channel.id, false); if (models.length && !prepareRecordsOnly) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'removeCurrentUserFromChannel'); } } return {models}; @@ -145,7 +145,7 @@ export async function setChannelDeleteAt(serverUrl: string, channelId: string, d const model = channel.prepareUpdate((c) => { c.deleteAt = deleteAt; }); - await operator.batchRecords([model]); + await operator.batchRecords([model], 'setChannelDeleteAt'); } catch (error) { logError('FAILED TO BATCH CHANGES FOR CHANNEL DELETE AT', error); } @@ -179,7 +179,7 @@ export async function markChannelAsViewed(serverUrl: string, channelId: string, }); PushNotifications.removeChannelNotifications(serverUrl, channelId); if (!prepareRecordsOnly) { - await operator.batchRecords([member]); + await operator.batchRecords([member], 'markChannelAsViewed'); } return {member}; @@ -206,7 +206,7 @@ export async function markChannelAsUnread(serverUrl: string, channelId: string, m.isUnread = true; }); if (!prepareRecordsOnly) { - await operator.batchRecords([member]); + await operator.batchRecords([member], 'markChannelAsUnread'); } return {member}; @@ -226,7 +226,7 @@ export async function resetMessageCount(serverUrl: string, channelId: string) { member.prepareUpdate((m) => { m.messageCount = 0; }); - await operator.batchRecords([member]); + await operator.batchRecords([member], 'resetMessageCount'); return member; } catch (error) { @@ -254,7 +254,7 @@ export async function storeMyChannelsForTeam(serverUrl: string, teamId: string, } if (flattenedModels.length) { - await operator.batchRecords(flattenedModels); + await operator.batchRecords(flattenedModels, 'storeMyChannelsForTeam'); } return {models: flattenedModels}; @@ -273,7 +273,7 @@ export async function updateMyChannelFromWebsocket(serverUrl: string, channelMem m.roles = channelMember.roles; }); if (!prepareRecordsOnly) { - operator.batchRecords([member]); + operator.batchRecords([member], 'updateMyChannelFromWebsocket'); } } return {model: member}; @@ -293,7 +293,7 @@ export async function updateChannelInfoFromChannel(serverUrl: string, channel: C }], prepareRecordsOnly: true}); if (!prepareRecordsOnly) { - operator.batchRecords(newInfo); + operator.batchRecords(newInfo, 'updateChannelInfoFromChannel'); } return {model: newInfo}; } catch (error) { @@ -317,7 +317,7 @@ export async function updateLastPostAt(serverUrl: string, channelId: string, las }); if (!prepareRecordsOnly) { - await operator.batchRecords([myChannel]); + await operator.batchRecords([myChannel], 'updateLastPostAt'); } return {member: myChannel}; @@ -345,7 +345,7 @@ export async function updateMyChannelLastFetchedAt(serverUrl: string, channelId: }); if (!prepareRecordsOnly) { - await operator.batchRecords([myChannel]); + await operator.batchRecords([myChannel], 'updateMyChannelLastFetchedAt'); } return {member: myChannel}; @@ -403,7 +403,7 @@ export async function updateChannelsDisplayName(serverUrl: string, channels: Cha } if (models.length && !prepareRecordsOnly) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'updateChannelsDisplayName'); } return {models}; diff --git a/app/actions/local/draft.ts b/app/actions/local/draft.ts index 35aa0d4d8..599875d0a 100644 --- a/app/actions/local/draft.ts +++ b/app/actions/local/draft.ts @@ -26,7 +26,7 @@ export async function updateDraftFile(serverUrl: string, channelId: string, root }); if (!prepareRecordsOnly) { - await operator.batchRecords([draft]); + await operator.batchRecords([draft], 'updateDraftFile'); } return {draft}; @@ -58,7 +58,7 @@ export async function removeDraftFile(serverUrl: string, channelId: string, root } if (!prepareRecordsOnly) { - await operator.batchRecords([draft]); + await operator.batchRecords([draft], 'removeDraftFile'); } return {draft}; @@ -99,7 +99,7 @@ export async function updateDraftMessage(serverUrl: string, channelId: string, r } if (!prepareRecordsOnly) { - await operator.batchRecords([draft]); + await operator.batchRecords([draft], 'updateDraftMessage'); } return {draft}; @@ -129,7 +129,7 @@ export async function addFilesToDraft(serverUrl: string, channelId: string, root }); if (!prepareRecordsOnly) { - await operator.batchRecords([draft]); + await operator.batchRecords([draft], 'addFilesToDraft'); } return {draft}; diff --git a/app/actions/local/post.ts b/app/actions/local/post.ts index 5f8b99fdc..dad5584c3 100644 --- a/app/actions/local/post.ts +++ b/app/actions/local/post.ts @@ -127,14 +127,14 @@ export async function removePost(serverUrl: string, post: PostModel | Post) { } if (removeModels.length) { - await operator.batchRecords(removeModels); + await operator.batchRecords(removeModels, 'removePost (combined user activity)'); } } else { const postModel = await getPostById(database, post.id); if (postModel) { const preparedPost = await prepareDeletePost(postModel); if (preparedPost.length) { - await operator.batchRecords(preparedPost); + await operator.batchRecords(preparedPost, 'removePost'); } } } @@ -162,7 +162,7 @@ export async function markPostAsDeleted(serverUrl: string, post: Post, prepareRe }); if (!prepareRecordsOnly) { - await operator.batchRecords([dbPost]); + await operator.batchRecords([dbPost], 'markPostAsDeleted'); } return {model}; } catch (error) { @@ -229,7 +229,7 @@ export async function storePostsForChannel( } if (models.length && !prepareRecordsOnly) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'storePostsForChannel'); } return {models}; diff --git a/app/actions/local/team.ts b/app/actions/local/team.ts index d8213183c..bfcd8ae5b 100644 --- a/app/actions/local/team.ts +++ b/app/actions/local/team.ts @@ -22,7 +22,7 @@ export async function removeUserFromTeam(serverUrl: string, teamId: string) { models.push(...system); } if (models.length) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'removeUserFromTeam'); } } @@ -61,7 +61,7 @@ export async function addSearchToTeamSearchHistory(serverUrl: string, teamId: st } } - await operator.batchRecords(models); + await operator.batchRecords(models, 'addSearchToTeamHistory'); return {searchModel}; } catch (error) { logError('Failed addSearchToTeamSearchHistory', error); diff --git a/app/actions/local/thread.ts b/app/actions/local/thread.ts index ed003bcee..9d95670dc 100644 --- a/app/actions/local/thread.ts +++ b/app/actions/local/thread.ts @@ -40,7 +40,7 @@ export const switchToGlobalThreads = async (serverUrl: string, teamId?: string, models.push(...history); if (!prepareRecordsOnly) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'switchToGlobalThreads'); } const isTabletDevice = await isTablet(); @@ -84,7 +84,7 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo currentChannelId: channel.id, }); if (models.length) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'switchToThread'); } } else { const modelPromises: Array> = []; @@ -97,7 +97,7 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo modelPromises.push(prepareCommonSystemValues(operator, commonValues)); const models = (await Promise.all(modelPromises)).flat(); if (models.length) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'switchToThread'); } } @@ -201,7 +201,7 @@ export async function createThreadFromNewPost(serverUrl: string, post: Post, pre } if (!prepareRecordsOnly) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'createThreadFromNewPost'); } return {models}; @@ -257,7 +257,7 @@ export async function processReceivedThreads(serverUrl: string, threads: Thread[ } if (!prepareRecordsOnly) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'processReceivedThreads'); } return {models}; } catch (error) { @@ -277,7 +277,7 @@ export async function markTeamThreadsAsRead(serverUrl: string, teamId: string, p record.viewedAt = Date.now(); })); if (!prepareRecordsOnly) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'markTeamThreadsAsRead'); } return {models}; } catch (error) { @@ -300,7 +300,7 @@ export async function markThreadAsViewed(serverUrl: string, threadId: string, pr }); if (!prepareRecordsOnly) { - await operator.batchRecords([thread]); + await operator.batchRecords([thread], 'markThreadAsViewed'); } return {model: thread}; @@ -327,7 +327,7 @@ export async function updateThread(serverUrl: string, threadId: string, updatedT record.unreadReplies = updatedThread.unread_replies ?? record.unreadReplies; }); if (!prepareRecordsOnly) { - await operator.batchRecords([model]); + await operator.batchRecords([model], 'updateThread'); } return {model}; } catch (error) { @@ -341,7 +341,7 @@ export async function updateTeamThreadsSync(serverUrl: string, data: TeamThreads const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const models = await operator.handleTeamThreadsSync({data: [data], prepareRecordsOnly}); if (!prepareRecordsOnly) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'updateTeamThreadsSync'); } return {models}; } catch (error) { diff --git a/app/actions/local/user.ts b/app/actions/local/user.ts index bbae45191..7c6bc260b 100644 --- a/app/actions/local/user.ts +++ b/app/actions/local/user.ts @@ -22,7 +22,7 @@ export async function setCurrentUserStatusOffline(serverUrl: string) { } user.prepareStatus(General.OFFLINE); - await operator.batchRecords([user]); + await operator.batchRecords([user], 'setCurrentUserStatusOffline'); return null; } catch (error) { logError('Failed setCurrentUserStatusOffline', error); @@ -54,7 +54,7 @@ export async function updateLocalCustomStatus(serverUrl: string, user: UserModel } } - await operator.batchRecords(models); + await operator.batchRecords(models, 'updateLocalCustomStatus'); return {}; } catch (error) { diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index 8bc9ba0b3..b52ae1d9b 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -174,7 +174,7 @@ export async function addMembersToChannel(serverUrl: string, channelId: string, })); const models = await Promise.all(modelPromises); - await operator.batchRecords(models.flat()); + await operator.batchRecords(models.flat(), 'addMembersToChannel'); } return {channelMemberships}; } catch (error) { @@ -251,7 +251,7 @@ export async function createChannel(serverUrl: string, displayName: string, purp models.push(...categoriesModels.models); } if (models.length) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'createChannel'); } fetchChannelStats(serverUrl, channelData.id, false); EphemeralStore.creatingChannel = false; @@ -296,7 +296,7 @@ export async function patchChannel(serverUrl: string, channelPatch: Partial 0) { try { - await operator.batchRecords(flattenedModels); + await operator.batchRecords(flattenedModels, 'joinChannel'); } catch { logError('FAILED TO BATCH CHANNELS'); } @@ -913,7 +913,7 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis models.push(...userModels); } - await operator.batchRecords(models); + await operator.batchRecords(models, 'createDirectChannel'); EphemeralStore.creatingDMorGMTeammates = []; fetchRolesIfNeeded(serverUrl, member.roles.split(' ')); return {data: created}; @@ -1047,7 +1047,7 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) { } models.push(...userModels); - operator.batchRecords(models); + operator.batchRecords(models, 'createGroupChannel'); } } EphemeralStore.creatingDMorGMTeammates = []; @@ -1392,7 +1392,7 @@ export const convertChannelToPrivate = async (serverUrl: string, channelId: stri channel.prepareUpdate((c) => { c.type = General.PRIVATE_CHANNEL; }); - await operator.batchRecords([channel]); + await operator.batchRecords([channel], 'convertChannelToPrivate'); } return {error: undefined}; } catch (error) { diff --git a/app/actions/remote/entry/app.ts b/app/actions/remote/entry/app.ts index d825ea2e7..217fb3182 100644 --- a/app/actions/remote/entry/app.ts +++ b/app/actions/remote/entry/app.ts @@ -33,7 +33,7 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) // clear lastUnreadChannelId const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''}); if (removeLastUnreadChannelId) { - await operator.batchRecords(removeLastUnreadChannelId); + await operator.batchRecords(removeLastUnreadChannelId, 'appEntry - removeLastUnreadChannelId'); } const {database} = operator; @@ -58,14 +58,14 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) currentChannelId: isTabletDevice ? initialChannelId : undefined, }); if (me?.length) { - await operator.batchRecords(me); + await operator.batchRecords(me, 'appEntry - upgrade store me'); } } await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId, currentChannelId, initialTeamId, initialChannelId); const dt = Date.now(); - await operator.batchRecords(models); + await operator.batchRecords(models, 'appEntry'); logInfo('ENTRY MODELS BATCHING TOOK', `${Date.now() - dt}ms`); setTeamLoading(serverUrl, false); diff --git a/app/actions/remote/entry/common.ts b/app/actions/remote/entry/common.ts index 631efa466..86fb98cb3 100644 --- a/app/actions/remote/entry/common.ts +++ b/app/actions/remote/entry/common.ts @@ -431,7 +431,7 @@ const graphQLSyncAllChannelMembers = async (serverUrl: string) => { const modelPromises = await prepareMyChannelsForTeam(operator, '', channels, memberships, undefined, true); const models = (await Promise.all(modelPromises)).flat(); if (models.length) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'graphQLSyncAllChannelMembers'); } } diff --git a/app/actions/remote/entry/gql_common.ts b/app/actions/remote/entry/gql_common.ts index 04840656f..b8f79eb74 100644 --- a/app/actions/remote/entry/gql_common.ts +++ b/app/actions/remote/entry/gql_common.ts @@ -82,7 +82,7 @@ export async function deferredAppEntryGraphQLActions( modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true})); } const models = (await Promise.all(modelPromises)).flat(); - operator.batchRecords(models); + operator.batchRecords(models, 'deferredAppEntryActions'); setTimeout(() => { if (result.chData?.channels?.length && result.chData.memberships?.length) { diff --git a/app/actions/remote/entry/login.ts b/app/actions/remote/entry/login.ts index 751bafe0c..e7673dc4c 100644 --- a/app/actions/remote/entry/login.ts +++ b/app/actions/remote/entry/login.ts @@ -68,7 +68,7 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs) setCurrentTeamAndChannelId(operator, initialTeamId, ''); } - await operator.batchRecords(models); + await operator.batchRecords(models, 'loginEntry'); setTeamLoading(serverUrl, false); const config = clData.config || {} as ClientConfig; diff --git a/app/actions/remote/entry/notification.ts b/app/actions/remote/entry/notification.ts index e3d57ac68..8ce004b7b 100644 --- a/app/actions/remote/entry/notification.ts +++ b/app/actions/remote/entry/notification.ts @@ -136,7 +136,7 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not await NavigationStore.waitUntilScreenHasLoaded(Screens.THREAD); } - await operator.batchRecords(models); + await operator.batchRecords(models, 'pushNotificationEntry'); setTeamLoading(serverUrl, false); const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!; diff --git a/app/actions/remote/file.ts b/app/actions/remote/file.ts index 60fe5dd13..e0f9235ac 100644 --- a/app/actions/remote/file.ts +++ b/app/actions/remote/file.ts @@ -3,6 +3,7 @@ import {DOWNLOAD_TIMEOUT} from '@constants/network'; import NetworkManager from '@managers/network_manager'; +import {logDebug} from '@utils/log'; import {forceLogoutIfNecessary} from './session'; @@ -32,10 +33,11 @@ export const uploadFile = ( let client: Client; try { client = NetworkManager.getClient(serverUrl); + return {cancel: client.uploadPostAttachment(file, channelId, onProgress, onComplete, onError, skipBytes)}; } catch (error) { + logDebug('uploadFile', error); return {error: error as ClientError}; } - return {cancel: client.uploadPostAttachment(file, channelId, onProgress, onComplete, onError, skipBytes)}; }; export const fetchPublicLink = async (serverUrl: string, fileId: string) => { diff --git a/app/actions/remote/groups.ts b/app/actions/remote/groups.ts index cd6997d21..aceb67560 100644 --- a/app/actions/remote/groups.ts +++ b/app/actions/remote/groups.ts @@ -83,7 +83,7 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string ]); if (!fetchOnly) { - await operator.batchRecords([...groups, ...groupChannels]); + await operator.batchRecords([...groups, ...groupChannels], 'fetchGroupsForChannel'); } return {groups, groupChannels}; @@ -110,7 +110,7 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetc ]); if (!fetchOnly) { - await operator.batchRecords([...groups, ...groupTeams]); + await operator.batchRecords([...groups, ...groupTeams], 'fetchGroupsForTeam'); } return {groups, groupTeams}; @@ -136,7 +136,7 @@ export const fetchGroupsForMember = async (serverUrl: string, userId: string, fe ]); if (!fetchOnly) { - await operator.batchRecords([...groups, ...groupMemberships]); + await operator.batchRecords([...groups, ...groupMemberships], 'fetchGroupsForMember'); } return {groups, groupMemberships}; diff --git a/app/actions/remote/post.ts b/app/actions/remote/post.ts index be2641b92..45e37cf92 100644 --- a/app/actions/remote/post.ts +++ b/app/actions/remote/post.ts @@ -128,7 +128,7 @@ export async function createPost(serverUrl: string, post: Partial, files: initialPostModels.push(...reactionModels); } - await operator.batchRecords(initialPostModels); + await operator.batchRecords(initialPostModels, 'createPost - initial'); const isCRTEnabled = await getIsCRTEnabled(database); @@ -167,7 +167,7 @@ export async function createPost(serverUrl: string, post: Partial, files: models.push(...threadModels); } } - await operator.batchRecords(models); + await operator.batchRecords(models, 'createPost - failure'); } return {data: true}; @@ -192,7 +192,7 @@ export async function createPost(serverUrl: string, post: Partial, files: models.push(...threadModels); } } - await operator.batchRecords(models); + await operator.batchRecords(models, 'createPost - success'); newPost = created; @@ -237,7 +237,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => { p.props = newPost.props; p.updateAt = timestamp; }); - await operator.batchRecords([post]); + await operator.batchRecords([post], 'retryFailedPost - first update'); const created = await client.createPost(newPost); const models = await operator.handlePosts({ @@ -253,7 +253,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => { models.push(member); } } - await operator.batchRecords(models); + await operator.batchRecords(models, 'retryFailedPost - success update'); } catch (error) { if (isServerError(error) && ( error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR || @@ -268,7 +268,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => { failed: true, }; }); - await operator.batchRecords([post]); + await operator.batchRecords([post], 'retryFailedPost - error update'); } return {error}; @@ -375,7 +375,7 @@ export async function fetchPosts(serverUrl: string, channelId: string, page = 0, models.push(...threadModels); } } - await operator.batchRecords(models); + await operator.batchRecords(models, 'fetchPosts'); } return result; } catch (error) { @@ -434,7 +434,7 @@ export async function fetchPostsBefore(serverUrl: string, channelId: string, pos } } - await operator.batchRecords(models); + await operator.batchRecords(models, 'fetchPostsBefore'); } catch (error) { logError('FETCH POSTS BEFORE ERROR', error); } @@ -489,7 +489,7 @@ export async function fetchPostsSince(serverUrl: string, channelId: string, sinc models.push(...threadModels); } } - await operator.batchRecords(models); + await operator.batchRecords(models, 'fetchPostsSince'); } return result; } catch (error) { @@ -621,7 +621,7 @@ export async function fetchPostThread(serverUrl: string, postId: string, options models.push(...threadModels); } } - await operator.batchRecords(models); + await operator.batchRecords(models, 'fetchPostThread'); } setFetchingThreadState(postId, false); return {posts: extractRecordsForTable(posts, MM_TABLES.SERVER.POST)}; @@ -697,7 +697,7 @@ export async function fetchPostsAround(serverUrl: string, channelId: string, pos models.push(...threadModels); } } - await operator.batchRecords(models); + await operator.batchRecords(models, 'fetchPostsAround'); } return {posts: extractRecordsForTable(posts, MM_TABLES.SERVER.POST)}; @@ -751,7 +751,7 @@ export async function fetchMissingChannelsFromPosts(serverUrl: string, posts: Po return mdls; }); if (models.length) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'fetchMissingChannelsFromPosts'); } } } @@ -809,7 +809,7 @@ export async function fetchPostById(serverUrl: string, postId: string, fetchOnly } } - await operator.batchRecords(models); + await operator.batchRecords(models, 'fetchPostById'); } return {post}; @@ -1036,7 +1036,7 @@ export async function fetchSavedPosts(serverUrl: string, teamId?: string, channe return mdls; }); - await operator.batchRecords(models); + await operator.batchRecords(models, 'fetchSavedPosts'); return { order, @@ -1118,7 +1118,7 @@ export async function fetchPinnedPosts(serverUrl: string, channelId: string) { return mdls; }); - await operator.batchRecords(models); + await operator.batchRecords(models, 'fetchPinnedPosts'); return { order, diff --git a/app/actions/remote/reactions.ts b/app/actions/remote/reactions.ts index 5537acbf2..cfb20f0b0 100644 --- a/app/actions/remote/reactions.ts +++ b/app/actions/remote/reactions.ts @@ -51,7 +51,7 @@ export async function addReaction(serverUrl: string, postId: string, emojiName: models.push(...recent); } - await operator.batchRecords(models); + await operator.batchRecords(models, 'addReaction'); return {reaction}; } diff --git a/app/actions/remote/retry.ts b/app/actions/remote/retry.ts index a71dec054..2a0b79d62 100644 --- a/app/actions/remote/retry.ts +++ b/app/actions/remote/retry.ts @@ -117,7 +117,7 @@ export async function retryInitialTeamAndChannel(serverUrl: string) { ), ])).flat(); - await operator.batchRecords(models); + await operator.batchRecords(models, 'retryInitialTeamAndChannel'); const directChannels = chData!.channels!.filter(isDMorGM); const channelsToFetchProfiles = new Set(directChannels); @@ -196,7 +196,7 @@ export async function retryInitialChannel(serverUrl: string, teamId: string) { prepareCommonSystemValues(operator, {currentChannelId: initialChannel?.id}), ])).flat(); - await operator.batchRecords(models); + await operator.batchRecords(models, 'retryInitialChannel'); const directChannels = chData!.channels!.filter(isDMorGM); const channelsToFetchProfiles = new Set(directChannels); diff --git a/app/actions/remote/search.ts b/app/actions/remote/search.ts index d3e40d899..8523b67b7 100644 --- a/app/actions/remote/search.ts +++ b/app/actions/remote/search.ts @@ -103,7 +103,7 @@ export const searchPosts = async (serverUrl: string, teamId: string, params: Pos return mdls; }); - await operator.batchRecords(models); + await operator.batchRecords(models, 'searchPosts'); return { order, posts: postsArray, diff --git a/app/actions/remote/team.ts b/app/actions/remote/team.ts index 2508e91b4..aa3006887 100644 --- a/app/actions/remote/team.ts +++ b/app/actions/remote/team.ts @@ -86,7 +86,7 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s prepareCategoriesAndCategoriesChannels(operator, categories || [], true), ])).flat(); - await operator.batchRecords(models); + await operator.batchRecords(models, 'addUserToTeam'); setTeamLoading(serverUrl, false); loadEventSent = false; @@ -199,7 +199,7 @@ export async function fetchMyTeams(serverUrl: string, fetchOnly = false): Promis const models = await Promise.all(modelPromises); const flattenedModels = models.flat(); if (flattenedModels.length > 0) { - await operator.batchRecords(flattenedModels); + await operator.batchRecords(flattenedModels, 'fetchMyTeams'); } } } @@ -233,7 +233,7 @@ export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly = const models = await Promise.all(modelPromises); const flattenedModels = models.flat(); if (flattenedModels?.length > 0) { - await operator.batchRecords(flattenedModels); + await operator.batchRecords(flattenedModels, 'fetchMyTeam'); } } } @@ -362,7 +362,7 @@ export async function fetchTeamByName(serverUrl: string, teamName: string, fetch const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (operator) { const models = await operator.handleTeam({teams: [team], prepareRecordsOnly: true}); - await operator.batchRecords(models); + await operator.batchRecords(models, 'fetchTeamByName'); } } @@ -441,7 +441,7 @@ export async function handleTeamChange(serverUrl: string, teamId: string) { } if (models.length) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'handleTeamChange'); } DeviceEventEmitter.emit(Events.TEAM_SWITCH, false); diff --git a/app/actions/remote/terms_of_service.ts b/app/actions/remote/terms_of_service.ts index 64cb865a0..da92f24e0 100644 --- a/app/actions/remote/terms_of_service.ts +++ b/app/actions/remote/terms_of_service.ts @@ -43,7 +43,7 @@ export async function updateTermsOfServiceStatus(serverUrl: string, id: string, u.termsOfServiceId = ''; } }); - operator.batchRecords([currentUser]); + operator.batchRecords([currentUser], 'updateTermsOfServiceStatus'); } return {resp}; } catch (error) { diff --git a/app/actions/remote/thread.ts b/app/actions/remote/thread.ts index e25a9f131..1de2d241a 100644 --- a/app/actions/remote/thread.ts +++ b/app/actions/remote/thread.ts @@ -395,7 +395,7 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare if (!prepareRecordsOnly && models?.length) { try { - await operator.batchRecords(models); + await operator.batchRecords(models, 'syncTeamThreads'); } catch (err) { if (__DEV__) { throw err; @@ -460,7 +460,7 @@ export const loadEarlierThreads = async (serverUrl: string, teamId: string, last if (!prepareRecordsOnly && models?.length) { try { - await operator.batchRecords(models); + await operator.batchRecords(models, 'loadEarlierThreads'); } catch (err) { if (__DEV__) { throw err; diff --git a/app/actions/remote/user.ts b/app/actions/remote/user.ts index cc6153fc2..5a2ae19e9 100644 --- a/app/actions/remote/user.ts +++ b/app/actions/remote/user.ts @@ -102,7 +102,7 @@ export async function fetchProfilesInChannel(serverUrl: string, channelId: strin modelPromises.push(prepare); const models = await Promise.all(modelPromises); - await operator.batchRecords(models.flat()); + await operator.batchRecords(models.flat(), 'fetchProfilesInChannel'); } } @@ -178,7 +178,7 @@ export async function fetchProfilesInGroupChannels(serverUrl: string, groupChann } const models = await Promise.all(modelPromises); - await operator.batchRecords(models.flat()); + await operator.batchRecords(models.flat(), 'fetchProfilesInGroupChannels'); } return {data}; @@ -230,7 +230,7 @@ export async function fetchProfilesPerChannels(serverUrl: string, channelIds: st } const models = await Promise.all(modelPromises); - await operator.batchRecords(models.flat()); + await operator.batchRecords(models.flat(), 'fetchProfilesPerChannels'); } return {data}; @@ -366,7 +366,7 @@ export async function fetchStatusByIds(serverUrl: string, userIds: string[], fet user.prepareStatus(status?.status || General.OFFLINE); } - await operator.batchRecords(users); + await operator.batchRecords(users, 'fetchStatusByIds'); } } @@ -641,7 +641,7 @@ export async function updateAllUsersSince(serverUrl: string, since: number, fetc modelsToBatch.push(...models); } - await operator.batchRecords(modelsToBatch); + await operator.batchRecords(modelsToBatch, 'updateAllUsersSince'); } } catch { // Do nothing @@ -677,7 +677,7 @@ export async function updateUsersNoLongerVisible(serverUrl: string, prepareRecor } } if (models.length && !prepareRecordsOnly) { - serverDatabase.operator.batchRecords(models); + serverDatabase.operator.batchRecords(models, 'updateUsersNoLongerVisible'); } } catch (error) { forceLogoutIfNecessary(serverUrl, error as ClientError); @@ -917,7 +917,7 @@ export const fetchTeamAndChannelMembership = async (serverUrl: string, userId: s } const models = await Promise.all(modelPromises); - await operator.batchRecords(models.flat()); + await operator.batchRecords(models.flat(), 'fetchTeamAndChannelMembership'); return {error: undefined}; } catch (error) { return {error}; diff --git a/app/actions/websocket/category.ts b/app/actions/websocket/category.ts index 7dc5c0e3d..2d4b06daa 100644 --- a/app/actions/websocket/category.ts +++ b/app/actions/websocket/category.ts @@ -96,7 +96,7 @@ export async function handleCategoryOrderUpdatedEvent(serverUrl: string, msg: We c.sortOrder = order.findIndex(findOrder); }); }); - await operator.batchRecords(categories); + await operator.batchRecords(categories, 'handleCategoryOrderUpdatedEvent'); } } catch (e) { logError('Category WS: handleCategoryOrderUpdatedEvent', e, msg); diff --git a/app/actions/websocket/channel.ts b/app/actions/websocket/channel.ts index 9d29e7346..08ac0e8da 100644 --- a/app/actions/websocket/channel.ts +++ b/app/actions/websocket/channel.ts @@ -57,7 +57,7 @@ export async function handleChannelCreatedEvent(serverUrl: string, msg: any) { } } } - operator.batchRecords(models); + operator.batchRecords(models, 'handleChannelCreatedEvent'); } catch { // do nothing } @@ -109,7 +109,7 @@ export async function handleChannelUpdatedEvent(serverUrl: string, msg: any) { if (infoModel.model) { models.push(...infoModel.model); } - operator.batchRecords(models); + operator.batchRecords(models, 'handleChannelUpdatedEvent'); } catch { // Do nothing } @@ -165,7 +165,7 @@ export async function handleChannelMemberUpdatedEvent(serverUrl: string, msg: an if (rolesRequest.roles?.length) { models.push(...await operator.handleRole({roles: rolesRequest.roles, prepareRecordsOnly: true})); } - operator.batchRecords(models); + operator.batchRecords(models, 'handleChannelMemberUpdatedEvent'); } catch { // do nothing } @@ -235,7 +235,7 @@ export async function handleDirectAddedEvent(serverUrl: string, msg: WebSocketMe models.push(...userModels); } - operator.batchRecords(models); + operator.batchRecords(models, 'handleDirectAddedEvent'); } catch { // do nothing } @@ -267,7 +267,7 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any) const prepareModels = await Promise.all(prepare); const flattenedModels = prepareModels.flat(); if (flattenedModels?.length > 0) { - await operator.batchRecords(flattenedModels); + await operator.batchRecords(flattenedModels, 'handleUserAddedToChannelEvent - prepareMyChannelsForTeam'); } } @@ -308,7 +308,7 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any) } if (models.length) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'handleUserAddedToChannelEvent'); } await fetchChannelStats(serverUrl, channelId, false); @@ -356,7 +356,7 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg: } } - operator.batchRecords(models); + operator.batchRecords(models, 'handleUserRemovedFromChannelEvent'); } catch (error) { logDebug('cannot handle user removed from channel websocket event', error); } diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index 7f06dfa86..d541af523 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -150,7 +150,7 @@ async function doReconnect(serverUrl: string) { await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeam?.id || '', currentChannel?.id || '', initialTeamId, initialChannelId); const dt = Date.now(); - await operator.batchRecords(models); + await operator.batchRecords(models, 'doReconnect'); logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`); setTeamLoading(serverUrl, false); diff --git a/app/actions/websocket/posts.ts b/app/actions/websocket/posts.ts index ead0530a1..6c16447cc 100644 --- a/app/actions/websocket/posts.ts +++ b/app/actions/websocket/posts.ts @@ -176,7 +176,7 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag models.push(...postModels); - operator.batchRecords(models); + operator.batchRecords(models, 'handleNewPostEvent'); } export async function handlePostEdited(serverUrl: string, msg: WebSocketMessage) { @@ -220,7 +220,7 @@ export async function handlePostEdited(serverUrl: string, msg: WebSocketMessage) }); models.push(...postModels); - operator.batchRecords(models); + operator.batchRecords(models, 'handlePostEdited'); } export async function handlePostDeleted(serverUrl: string, msg: WebSocketMessage) { @@ -263,7 +263,7 @@ export async function handlePostDeleted(serverUrl: string, msg: WebSocketMessage } if (models.length) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'handlePostDeleted'); } } catch { // Do nothing diff --git a/app/actions/websocket/roles.ts b/app/actions/websocket/roles.ts index 8cb73847d..ee535f3ac 100644 --- a/app/actions/websocket/roles.ts +++ b/app/actions/websocket/roles.ts @@ -66,7 +66,7 @@ export async function handleUserRoleUpdatedEvent(serverUrl: string, msg: WebSock models.push(user); } - await operator.batchRecords(models); + await operator.batchRecords(models, 'handleUserRoleUpdatedEvent'); } export async function handleTeamMemberRoleUpdatedEvent(serverUrl: string, msg: WebSocketMessage): Promise { @@ -118,7 +118,7 @@ export async function handleTeamMemberRoleUpdatedEvent(serverUrl: string, msg: W }); models.push(...teamMembership); - await operator.batchRecords(models); + await operator.batchRecords(models, 'handleTeamMemberRoleUpdatedEvent'); } catch { // do nothing } diff --git a/app/actions/websocket/teams.ts b/app/actions/websocket/teams.ts index afd4ba6dd..8150edfd4 100644 --- a/app/actions/websocket/teams.ts +++ b/app/actions/websocket/teams.ts @@ -162,5 +162,5 @@ const fetchAndStoreJoinedTeamInfo = async (serverUrl: string, operator: ServerDa } const models = await Promise.all(modelPromises); - await operator.batchRecords(models.flat()); + await operator.batchRecords(models.flat(), 'fetchAndStoreJoinedTeamInfo'); }; diff --git a/app/actions/websocket/users.ts b/app/actions/websocket/users.ts index 089fe786d..2c9e3a561 100644 --- a/app/actions/websocket/users.ts +++ b/app/actions/websocket/users.ts @@ -71,7 +71,7 @@ export async function handleUserUpdatedEvent(serverUrl: string, msg: WebSocketMe modelsToBatch.push(...userModel); try { - await operator.batchRecords(modelsToBatch); + await operator.batchRecords(modelsToBatch, 'handleUserUpdatedEvent'); } catch { // do nothing } diff --git a/app/client/rest/apps.ts b/app/client/rest/apps.ts index 11f045d9b..e80712460 100644 --- a/app/client/rest/apps.ts +++ b/app/client/rest/apps.ts @@ -3,13 +3,15 @@ import {buildQueryString} from '@utils/helpers'; +import type ClientBase from './base'; + export interface ClientAppsMix { executeAppCall: (call: AppCallRequest, trackAsSubmit: boolean) => Promise>; getAppsBindings: (userID: string, channelID: string, teamID: string) => Promise; } -const ClientApps = (superclass: any) => class extends superclass { - executeAppCall = async (call: AppCallRequest, trackAsSubmit: boolean): Promise> => { +const ClientApps = >(superclass: TBase) => class extends superclass { + executeAppCall = async (call: AppCallRequest, trackAsSubmit: boolean) => { const callCopy = { ...call, context: { diff --git a/app/client/rest/base.ts b/app/client/rest/base.ts index c2aa06885..95a8476fa 100644 --- a/app/client/rest/base.ts +++ b/app/client/rest/base.ts @@ -26,6 +26,7 @@ export default class ClientBase { requestHeaders: {[x: string]: string} = {}; serverVersion = ''; urlVersion = '/api/v4'; + enableLogging = false; constructor(apiClient: APIClientInterface, serverUrl: string, bearerToken?: string, csrfToken?: string) { this.apiClient = apiClient; @@ -45,6 +46,10 @@ export default class ClientBase { } } + getBaseRoute() { + return this.apiClient.baseUrl || ''; + } + getAbsoluteUrl(baseUrl?: string) { if (typeof baseUrl !== 'string' || !baseUrl.startsWith('/')) { return baseUrl; @@ -303,7 +308,7 @@ export default class ClientBase { } if (response.ok) { - return returnDataOnly ? response.data : response; + return returnDataOnly ? (response.data || {}) : response; } throw new ClientError(this.apiClient.baseUrl, { diff --git a/app/client/rest/categories.ts b/app/client/rest/categories.ts index f940bdeed..73e15820c 100644 --- a/app/client/rest/categories.ts +++ b/app/client/rest/categories.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import type ClientBase from './base'; + export interface ClientCategoriesMix { getCategories: (userId: string, teamId: string) => Promise; getCategoriesOrder: (userId: string, teamId: string) => Promise; @@ -8,7 +10,7 @@ export interface ClientCategoriesMix { updateChannelCategories: (userId: string, teamId: string, categories: CategoryWithChannels[]) => Promise; } -const ClientCategories = (superclass: any) => class extends superclass { +const ClientCategories = >(superclass: TBase) => class extends superclass { getCategories = async (userId: string, teamId: string) => { return this.doFetch( `${this.getCategoriesRoute(userId, teamId)}`, diff --git a/app/client/rest/channels.ts b/app/client/rest/channels.ts index b5adb465a..ce9799eb8 100644 --- a/app/client/rest/channels.ts +++ b/app/client/rest/channels.ts @@ -5,6 +5,8 @@ import {buildQueryString} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; +import type ClientBase from './base'; + export interface ClientChannelsMix { getAllChannels: (page?: number, perPage?: number, notAssociatedToGroup?: string, excludeDefaultChannels?: boolean, includeTotalCount?: boolean) => Promise; createChannel: (channel: Channel) => Promise; @@ -44,7 +46,7 @@ export interface ClientChannelsMix { getMemberInChannel: (channelId: string, userId: string) => Promise; } -const ClientChannels = (superclass: any) => class extends superclass { +const ClientChannels = >(superclass: TBase) => class extends superclass { getAllChannels = async (page = 0, perPage = PER_PAGE_DEFAULT, notAssociatedToGroup = '', excludeDefaultChannels = false, includeTotalCount = false) => { const queryData = { page, @@ -60,7 +62,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; createChannel = async (channel: Channel) => { - this.analytics.trackAPI('api_channels_create', {team_id: channel.team_id}); + this.analytics?.trackAPI('api_channels_create', {team_id: channel.team_id}); return this.doFetch( `${this.getChannelsRoute()}`, @@ -69,7 +71,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; createDirectChannel = async (userIds: string[]) => { - this.analytics.trackAPI('api_channels_create_direct'); + this.analytics?.trackAPI('api_channels_create_direct'); return this.doFetch( `${this.getChannelsRoute()}/direct`, @@ -78,7 +80,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; createGroupChannel = async (userIds: string[]) => { - this.analytics.trackAPI('api_channels_create_group'); + this.analytics?.trackAPI('api_channels_create_group'); return this.doFetch( `${this.getChannelsRoute()}/group`, @@ -87,7 +89,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; deleteChannel = async (channelId: string) => { - this.analytics.trackAPI('api_channels_delete', {channel_id: channelId}); + this.analytics?.trackAPI('api_channels_delete', {channel_id: channelId}); return this.doFetch( `${this.getChannelRoute(channelId)}`, @@ -96,7 +98,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; unarchiveChannel = async (channelId: string) => { - this.analytics.trackAPI('api_channels_unarchive', {channel_id: channelId}); + this.analytics?.trackAPI('api_channels_unarchive', {channel_id: channelId}); return this.doFetch( `${this.getChannelRoute(channelId)}/restore`, @@ -105,7 +107,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; updateChannel = async (channel: Channel) => { - this.analytics.trackAPI('api_channels_update', {channel_id: channel.id}); + this.analytics?.trackAPI('api_channels_update', {channel_id: channel.id}); return this.doFetch( `${this.getChannelRoute(channel.id)}`, @@ -118,7 +120,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; updateChannelPrivacy = async (channelId: string, privacy: any) => { - this.analytics.trackAPI('api_channels_update_privacy', {channel_id: channelId, privacy}); + this.analytics?.trackAPI('api_channels_update_privacy', {channel_id: channelId, privacy}); return this.doFetch( `${this.getChannelRoute(channelId)}/privacy`, @@ -127,7 +129,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; patchChannel = async (channelId: string, channelPatch: Partial) => { - this.analytics.trackAPI('api_channels_patch', {channel_id: channelId}); + this.analytics?.trackAPI('api_channels_patch', {channel_id: channelId}); return this.doFetch( `${this.getChannelRoute(channelId)}/patch`, @@ -136,7 +138,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; updateChannelNotifyProps = async (props: ChannelNotifyProps & {channel_id: string; user_id: string}) => { - this.analytics.trackAPI('api_users_update_channel_notifications', {channel_id: props.channel_id}); + this.analytics?.trackAPI('api_users_update_channel_notifications', {channel_id: props.channel_id}); return this.doFetch( `${this.getChannelMemberRoute(props.channel_id, props.user_id)}/notify_props`, @@ -145,7 +147,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; getChannel = async (channelId: string) => { - this.analytics.trackAPI('api_channel_get', {channel_id: channelId}); + this.analytics?.trackAPI('api_channel_get', {channel_id: channelId}); return this.doFetch( `${this.getChannelRoute(channelId)}`, @@ -161,7 +163,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; getChannelByNameAndTeamName = async (teamName: string, channelName: string, includeDeleted = false) => { - this.analytics.trackAPI('api_channel_get_by_name_and_teamName', {channel_name: channelName, team_name: teamName, include_deleted: includeDeleted}); + this.analytics?.trackAPI('api_channel_get_by_name_and_teamName', {channel_name: channelName, team_name: teamName, include_deleted: includeDeleted}); return this.doFetch( `${this.getTeamNameRoute(teamName)}/channels/name/${channelName}?include_deleted=${includeDeleted}`, @@ -243,7 +245,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; addToChannel = async (userId: string, channelId: string, postRootId = '') => { - this.analytics.trackAPI('api_channels_add_member', {channel_id: channelId}); + this.analytics?.trackAPI('api_channels_add_member', {channel_id: channelId}); const member = {user_id: userId, channel_id: channelId, post_root_id: postRootId}; return this.doFetch( @@ -253,7 +255,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; removeFromChannel = async (userId: string, channelId: string) => { - this.analytics.trackAPI('api_channels_remove_member', {channel_id: channelId}); + this.analytics?.trackAPI('api_channels_remove_member', {channel_id: channelId}); return this.doFetch( `${this.getChannelMemberRoute(channelId, userId)}`, diff --git a/app/client/rest/emojis.ts b/app/client/rest/emojis.ts index 5cd032ec5..020461aac 100644 --- a/app/client/rest/emojis.ts +++ b/app/client/rest/emojis.ts @@ -5,6 +5,8 @@ import {buildQueryString} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; +import type ClientBase from './base'; + export interface ClientEmojisMix { getCustomEmoji: (id: string) => Promise; getCustomEmojiByName: (name: string) => Promise; @@ -15,7 +17,7 @@ export interface ClientEmojisMix { autocompleteCustomEmoji: (name: string) => Promise; } -const ClientEmojis = (superclass: any) => class extends superclass { +const ClientEmojis = >(superclass: TBase) => class extends superclass { getCustomEmoji = async (id: string) => { return this.doFetch( `${this.getEmojisRoute()}/${id}`, diff --git a/app/client/rest/files.ts b/app/client/rest/files.ts index c4cd64836..0c849c1de 100644 --- a/app/client/rest/files.ts +++ b/app/client/rest/files.ts @@ -3,6 +3,7 @@ import {toMilliseconds} from '@utils/datetime'; +import type ClientBase from './base'; import type {ClientResponse, ClientResponseError, ProgressPromise, UploadRequestOptions} from '@mattermost/react-native-network-client'; export interface ClientFilesMix { @@ -22,7 +23,7 @@ export interface ClientFilesMix { searchFilesWithParams: (teamId: string, FileSearchParams: string) => Promise; } -const ClientFiles = (superclass: any) => class extends superclass { +const ClientFiles = >(superclass: TBase) => class extends superclass { getFileUrl(fileId: string, timestamp: number) { let url = `${this.apiClient.baseUrl}${this.getFileRoute(fileId)}`; if (timestamp) { @@ -76,13 +77,17 @@ const ClientFiles = (superclass: any) => class extends superclass { }, timeoutInterval: toMilliseconds({minutes: 3}), }; + if (!file.localPath) { + throw new Error('file does not have local path defined'); + } + const promise = this.apiClient.upload(url, file.localPath, options) as ProgressPromise; promise.progress!(onProgress).then(onComplete).catch(onError); return promise.cancel!; }; searchFilesWithParams = async (teamId: string, params: FileSearchParams) => { - this.analytics.trackAPI('api_files_search'); + this.analytics?.trackAPI('api_files_search'); const endpoint = teamId ? `${this.getTeamRoute(teamId)}/files/search` : `${this.getFilesRoute()}/search`; return this.doFetch(endpoint, {method: 'post', body: params}); }; diff --git a/app/client/rest/general.ts b/app/client/rest/general.ts index b4093ca25..9969dd062 100644 --- a/app/client/rest/general.ts +++ b/app/client/rest/general.ts @@ -6,6 +6,8 @@ import {buildQueryString} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; import ClientError from './error'; +import type ClientBase from './base'; + type PoliciesResponse = { policies: T[]; total_count: number; @@ -25,7 +27,7 @@ export interface ClientGeneralMix { getRedirectLocation: (urlParam: string) => Promise>; } -const ClientGeneral = (superclass: any) => class extends superclass { +const ClientGeneral = >(superclass: TBase) => class extends superclass { getOpenGraphMetadata = async (url: string) => { return this.doFetch( `${this.urlVersion}/opengraph`, @@ -49,7 +51,7 @@ const ClientGeneral = (superclass: any) => class extends superclass { const url = `${this.urlVersion}/logs`; if (!this.enableLogging) { - throw new ClientError(this.client.baseUrl, { + throw new ClientError(this.apiClient.baseUrl, { message: 'Logging disabled.', url, }); diff --git a/app/client/rest/groups.ts b/app/client/rest/groups.ts index 7b9829199..61b608f6b 100644 --- a/app/client/rest/groups.ts +++ b/app/client/rest/groups.ts @@ -5,6 +5,8 @@ import {buildQueryString} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; +import type ClientBase from './base'; + export interface ClientGroupsMix { getGroup: (id: string) => Promise; getGroups: (params: {query?: string; filterAllowReference?: boolean; page?: number; perPage?: number; since?: number; includeMemberCount?: boolean}) => Promise; @@ -16,7 +18,7 @@ export interface ClientGroupsMix { getAllTeamsAssociatedToGroup: (groupId: string, filterAllowReference?: boolean) => Promise<{groupTeams: GroupTeam[]}>; } -const ClientGroups = (superclass: any) => class extends superclass { +const ClientGroups = >(superclass: TBase) => class extends superclass { getGroup = async (id: string) => { return this.doFetch( `${this.urlVersion}/groups/${id}`, diff --git a/app/client/rest/integrations.ts b/app/client/rest/integrations.ts index 020f596d3..2ef8df304 100644 --- a/app/client/rest/integrations.ts +++ b/app/client/rest/integrations.ts @@ -5,6 +5,8 @@ import {buildQueryString} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; +import type ClientBase from './base'; + export interface ClientIntegrationsMix { getCommandsList: (teamId: string) => Promise; getCommandAutocompleteSuggestionsList: (userInput: string, teamId: string, channelId: string, rootId?: string) => Promise; @@ -14,7 +16,7 @@ export interface ClientIntegrationsMix { submitInteractiveDialog: (data: DialogSubmission) => Promise; } -const ClientIntegrations = (superclass: any) => class extends superclass { +const ClientIntegrations = >(superclass: TBase) => class extends superclass { getCommandsList = async (teamId: string) => { return this.doFetch( `${this.getCommandsRoute()}?team_id=${teamId}`, @@ -37,7 +39,7 @@ const ClientIntegrations = (superclass: any) => class extends superclass { }; executeCommand = async (command: string, commandArgs = {}) => { - this.analytics.trackAPI('api_integrations_used'); + this.analytics?.trackAPI('api_integrations_used'); return this.doFetch( `${this.getCommandsRoute()}/execute`, @@ -46,7 +48,7 @@ const ClientIntegrations = (superclass: any) => class extends superclass { }; addCommand = async (command: Command) => { - this.analytics.trackAPI('api_integrations_created'); + this.analytics?.trackAPI('api_integrations_created'); return this.doFetch( `${this.getCommandsRoute()}`, @@ -55,7 +57,7 @@ const ClientIntegrations = (superclass: any) => class extends superclass { }; submitInteractiveDialog = async (data: DialogSubmission) => { - this.analytics.trackAPI('api_interactive_messages_dialog_submitted'); + this.analytics?.trackAPI('api_interactive_messages_dialog_submitted'); return this.doFetch( `${this.urlVersion}/actions/dialogs/submit`, {method: 'post', body: data}, diff --git a/app/client/rest/nps.ts b/app/client/rest/nps.ts index c0c7c68bb..1964c0131 100644 --- a/app/client/rest/nps.ts +++ b/app/client/rest/nps.ts @@ -3,11 +3,13 @@ import {General} from '@constants'; +import type ClientBase from './base'; + export interface ClientNPSMix { npsGiveFeedbackAction: () => Promise; } -const ClientNPS = (superclass: any) => class extends superclass { +const ClientNPS = >(superclass: TBase) => class extends superclass { npsGiveFeedbackAction = async () => { return this.doFetch( `${this.getPluginRoute(General.NPS_PLUGIN_ID)}/api/v1/give_feedback`, diff --git a/app/client/rest/plugins.ts b/app/client/rest/plugins.ts index 7f876c879..82ab86924 100644 --- a/app/client/rest/plugins.ts +++ b/app/client/rest/plugins.ts @@ -1,11 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import type ClientBase from './base'; + export interface ClientPluginsMix { getPluginsManifests: () => Promise; } -const ClientPlugins = (superclass: any) => class extends superclass { +const ClientPlugins = >(superclass: TBase) => class extends superclass { getPluginsManifests = async () => { return this.doFetch( `${this.getPluginsRoute()}/webapp`, diff --git a/app/client/rest/posts.ts b/app/client/rest/posts.ts index 6075925c7..ce3943205 100644 --- a/app/client/rest/posts.ts +++ b/app/client/rest/posts.ts @@ -5,6 +5,8 @@ import {buildQueryString} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; +import type ClientBase from './base'; + export interface ClientPostsMix { createPost: (post: Post) => Promise; updatePost: (post: Post) => Promise; @@ -31,12 +33,12 @@ export interface ClientPostsMix { doPostActionWithCookie: (postId: string, actionId: string, actionCookie: string, selectedOption?: string) => Promise; } -const ClientPosts = (superclass: any) => class extends superclass { +const ClientPosts = >(superclass: TBase) => class extends superclass { createPost = async (post: Post) => { - this.analytics.trackAPI('api_posts_create', {channel_id: post.channel_id}); + this.analytics?.trackAPI('api_posts_create', {channel_id: post.channel_id}); if (post.root_id != null && post.root_id !== '') { - this.analytics.trackAPI('api_posts_replied', {channel_id: post.channel_id}); + this.analytics?.trackAPI('api_posts_replied', {channel_id: post.channel_id}); } return this.doFetch( @@ -46,7 +48,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; updatePost = async (post: Post) => { - this.analytics.trackAPI('api_posts_update', {channel_id: post.channel_id}); + this.analytics?.trackAPI('api_posts_update', {channel_id: post.channel_id}); return this.doFetch( `${this.getPostRoute(post.id)}`, @@ -62,7 +64,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; patchPost = async (postPatch: Partial & {id: string}) => { - this.analytics.trackAPI('api_posts_patch', {channel_id: postPatch.channel_id}); + this.analytics?.trackAPI('api_posts_patch', {channel_id: postPatch.channel_id}); return this.doFetch( `${this.getPostRoute(postPatch.id)}/patch`, @@ -71,7 +73,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; deletePost = async (postId: string) => { - this.analytics.trackAPI('api_posts_delete'); + this.analytics?.trackAPI('api_posts_delete'); return this.doFetch( `${this.getPostRoute(postId)}`, @@ -79,7 +81,7 @@ const ClientPosts = (superclass: any) => class extends superclass { ); }; - getPostThread = (postId: string, options: FetchPaginatedThreadOptions): Promise => { + getPostThread = (postId: string, options: FetchPaginatedThreadOptions) => { const { fetchThreads = true, collapsedThreads = false, @@ -110,7 +112,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; getPostsBefore = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT, collapsedThreads = false, collapsedThreadsExtended = false) => { - this.analytics.trackAPI('api_posts_get_before', {channel_id: channelId}); + this.analytics?.trackAPI('api_posts_get_before', {channel_id: channelId}); return this.doFetch( `${this.getChannelRoute(channelId)}/posts${buildQueryString({before: postId, page, per_page: perPage, collapsedThreads, collapsedThreadsExtended})}`, @@ -119,7 +121,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; getPostsAfter = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT, collapsedThreads = false, collapsedThreadsExtended = false) => { - this.analytics.trackAPI('api_posts_get_after', {channel_id: channelId}); + this.analytics?.trackAPI('api_posts_get_after', {channel_id: channelId}); return this.doFetch( `${this.getChannelRoute(channelId)}/posts${buildQueryString({after: postId, page, per_page: perPage, collapsedThreads, collapsedThreadsExtended})}`, @@ -135,7 +137,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; getSavedPosts = async (userId: string, channelId = '', teamId = '', page = 0, perPage = PER_PAGE_DEFAULT) => { - this.analytics.trackAPI('api_posts_get_flagged', {team_id: teamId}); + this.analytics?.trackAPI('api_posts_get_flagged', {team_id: teamId}); return this.doFetch( `${this.getUserRoute(userId)}/posts/flagged${buildQueryString({channel_id: channelId, team_id: teamId, page, per_page: perPage})}`, @@ -144,7 +146,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; getPinnedPosts = async (channelId: string) => { - this.analytics.trackAPI('api_posts_get_pinned', {channel_id: channelId}); + this.analytics?.trackAPI('api_posts_get_pinned', {channel_id: channelId}); return this.doFetch( `${this.getChannelRoute(channelId)}/pinned`, {method: 'get'}, @@ -152,7 +154,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; markPostAsUnread = async (userId: string, postId: string) => { - this.analytics.trackAPI('api_post_set_unread_post'); + this.analytics?.trackAPI('api_post_set_unread_post'); // collapsed_threads_supported is not based on user preferences but to know if "CLIENT" supports CRT const body = JSON.stringify({collapsed_threads_supported: true}); @@ -164,7 +166,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; pinPost = async (postId: string) => { - this.analytics.trackAPI('api_posts_pin'); + this.analytics?.trackAPI('api_posts_pin'); return this.doFetch( `${this.getPostRoute(postId)}/pin`, @@ -173,7 +175,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; unpinPost = async (postId: string) => { - this.analytics.trackAPI('api_posts_unpin'); + this.analytics?.trackAPI('api_posts_unpin'); return this.doFetch( `${this.getPostRoute(postId)}/unpin`, @@ -182,7 +184,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; addReaction = async (userId: string, postId: string, emojiName: string) => { - this.analytics.trackAPI('api_reactions_save', {post_id: postId}); + this.analytics?.trackAPI('api_reactions_save', {post_id: postId}); return this.doFetch( `${this.getReactionsRoute()}`, @@ -191,7 +193,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; removeReaction = async (userId: string, postId: string, emojiName: string) => { - this.analytics.trackAPI('api_reactions_delete', {post_id: postId}); + this.analytics?.trackAPI('api_reactions_delete', {post_id: postId}); return this.doFetch( `${this.getUserRoute(userId)}/posts/${postId}/reactions/${emojiName}`, @@ -207,7 +209,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; searchPostsWithParams = async (teamId: string, params: PostSearchParams) => { - this.analytics.trackAPI('api_posts_search'); + this.analytics?.trackAPI('api_posts_search'); const endpoint = teamId ? `${this.getTeamRoute(teamId)}/posts/search` : `${this.getPostsRoute()}/search`; return this.doFetch(endpoint, {method: 'post', body: params}); }; @@ -222,9 +224,9 @@ const ClientPosts = (superclass: any) => class extends superclass { doPostActionWithCookie = async (postId: string, actionId: string, actionCookie: string, selectedOption = '') => { if (selectedOption) { - this.analytics.trackAPI('api_interactive_messages_menu_selected'); + this.analytics?.trackAPI('api_interactive_messages_menu_selected'); } else { - this.analytics.trackAPI('api_interactive_messages_button_clicked'); + this.analytics?.trackAPI('api_interactive_messages_button_clicked'); } const msg: any = { diff --git a/app/client/rest/preferences.ts b/app/client/rest/preferences.ts index db69d414b..f6a9893d4 100644 --- a/app/client/rest/preferences.ts +++ b/app/client/rest/preferences.ts @@ -1,15 +1,17 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import type ClientBase from './base'; + export interface ClientPreferencesMix { savePreferences: (userId: string, preferences: PreferenceType[]) => Promise; deletePreferences: (userId: string, preferences: PreferenceType[]) => Promise; getMyPreferences: () => Promise; } -const ClientPreferences = (superclass: any) => class extends superclass { +const ClientPreferences = >(superclass: TBase) => class extends superclass { savePreferences = async (userId: string, preferences: PreferenceType[]) => { - this.analytics.trackAPI('action_posts_flag'); + this.analytics?.trackAPI('action_posts_flag'); return this.doFetch( `${this.getPreferencesRoute(userId)}`, {method: 'put', body: preferences}, @@ -24,7 +26,7 @@ const ClientPreferences = (superclass: any) => class extends superclass { }; deletePreferences = async (userId: string, preferences: PreferenceType[]) => { - this.analytics.trackAPI('action_posts_unflag'); + this.analytics?.trackAPI('action_posts_unflag'); return this.doFetch( `${this.getPreferencesRoute(userId)}/delete`, {method: 'post', body: preferences}, diff --git a/app/client/rest/teams.ts b/app/client/rest/teams.ts index 190256568..8398351a4 100644 --- a/app/client/rest/teams.ts +++ b/app/client/rest/teams.ts @@ -5,6 +5,8 @@ import {buildQueryString} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; +import type ClientBase from './base'; + export interface ClientTeamsMix { createTeam: (team: Team) => Promise; deleteTeam: (teamId: string) => Promise; @@ -28,9 +30,9 @@ export interface ClientTeamsMix { getTeamIconUrl: (teamId: string, lastTeamIconUpdate: number) => string; } -const ClientTeams = (superclass: any) => class extends superclass { +const ClientTeams = >(superclass: TBase) => class extends superclass { createTeam = async (team: Team) => { - this.analytics.trackAPI('api_teams_create'); + this.analytics?.trackAPI('api_teams_create'); return this.doFetch( `${this.getTeamsRoute()}`, @@ -39,7 +41,7 @@ const ClientTeams = (superclass: any) => class extends superclass { }; deleteTeam = async (teamId: string) => { - this.analytics.trackAPI('api_teams_delete'); + this.analytics?.trackAPI('api_teams_delete'); return this.doFetch( `${this.getTeamRoute(teamId)}`, @@ -48,7 +50,7 @@ const ClientTeams = (superclass: any) => class extends superclass { }; updateTeam = async (team: Team) => { - this.analytics.trackAPI('api_teams_update_name', {team_id: team.id}); + this.analytics?.trackAPI('api_teams_update_name', {team_id: team.id}); return this.doFetch( `${this.getTeamRoute(team.id)}`, @@ -57,7 +59,7 @@ const ClientTeams = (superclass: any) => class extends superclass { }; patchTeam = async (team: Partial & {id: string}) => { - this.analytics.trackAPI('api_teams_patch_name', {team_id: team.id}); + this.analytics?.trackAPI('api_teams_patch_name', {team_id: team.id}); return this.doFetch( `${this.getTeamRoute(team.id)}/patch`, @@ -80,7 +82,7 @@ const ClientTeams = (superclass: any) => class extends superclass { }; getTeamByName = async (teamName: string) => { - this.analytics.trackAPI('api_teams_get_team_by_name'); + this.analytics?.trackAPI('api_teams_get_team_by_name'); return this.doFetch( this.getTeamNameRoute(teamName), @@ -131,7 +133,7 @@ const ClientTeams = (superclass: any) => class extends superclass { }; addToTeam = async (teamId: string, userId: string) => { - this.analytics.trackAPI('api_teams_invite_members', {team_id: teamId}); + this.analytics?.trackAPI('api_teams_invite_members', {team_id: teamId}); const member = {user_id: userId, team_id: teamId}; return this.doFetch( @@ -141,7 +143,7 @@ const ClientTeams = (superclass: any) => class extends superclass { }; addUsersToTeamGracefully = (teamId: string, userIds: string[]) => { - this.analytics.trackAPI('api_teams_batch_add_members', {team_id: teamId, count: userIds.length}); + this.analytics?.trackAPI('api_teams_batch_add_members', {team_id: teamId, count: userIds.length}); const members: Array<{team_id: string; user_id: string}> = []; userIds.forEach((id) => members.push({team_id: teamId, user_id: id})); @@ -153,7 +155,7 @@ const ClientTeams = (superclass: any) => class extends superclass { }; sendEmailInvitesToTeamGracefully = (teamId: string, emails: string[]) => { - this.analytics.trackAPI('api_teams_invite_members', {team_id: teamId}); + this.analytics?.trackAPI('api_teams_invite_members', {team_id: teamId}); return this.doFetch( `${this.getTeamRoute(teamId)}/invite/email?graceful=true`, @@ -170,7 +172,7 @@ const ClientTeams = (superclass: any) => class extends superclass { }; removeFromTeam = async (teamId: string, userId: string) => { - this.analytics.trackAPI('api_teams_remove_members', {team_id: teamId}); + this.analytics?.trackAPI('api_teams_remove_members', {team_id: teamId}); return this.doFetch( `${this.getTeamMemberRoute(teamId, userId)}`, diff --git a/app/client/rest/threads.ts b/app/client/rest/threads.ts index a4fbfce5d..c071c8fa1 100644 --- a/app/client/rest/threads.ts +++ b/app/client/rest/threads.ts @@ -5,6 +5,8 @@ import {buildQueryString, isMinimumServerVersion} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; +import type ClientBase from './base'; + export interface ClientThreadsMix { getThreads: (userId: string, teamId: string, before?: string, after?: string, pageSize?: number, deleted?: boolean, unread?: boolean, since?: number, totalsOnly?: boolean, serverVersion?: string) => Promise; getThread: (userId: string, teamId: string, threadId: string, extended?: boolean) => Promise; @@ -14,7 +16,7 @@ export interface ClientThreadsMix { updateThreadFollow: (userId: string, teamId: string, threadId: string, state: boolean) => Promise; } -const ClientThreads = (superclass: any) => class extends superclass { +const ClientThreads = >(superclass: TBase) => class extends superclass { getThreads = async (userId: string, teamId: string, before = '', after = '', pageSize = PER_PAGE_DEFAULT, deleted = false, unread = false, since = 0, totalsOnly = false, serverVersion = '') => { const queryStringObj: Record = { extended: 'true', diff --git a/app/client/rest/tos.ts b/app/client/rest/tos.ts index 0926ff461..03856a353 100644 --- a/app/client/rest/tos.ts +++ b/app/client/rest/tos.ts @@ -1,12 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import type ClientBase from './base'; + export interface ClientTosMix { updateMyTermsOfServiceStatus: (termsOfServiceId: string, accepted: boolean) => Promise<{status: string}>; getTermsOfService: () => Promise; } -const ClientTos = (superclass: any) => class extends superclass { +const ClientTos = >(superclass: TBase) => class extends superclass { updateMyTermsOfServiceStatus = async (termsOfServiceId: string, accepted: boolean) => { return this.doFetch( `${this.getUserRoute('me')}/terms_of_service`, diff --git a/app/client/rest/users.ts b/app/client/rest/users.ts index aac8d355e..047c1051d 100644 --- a/app/client/rest/users.ts +++ b/app/client/rest/users.ts @@ -6,6 +6,8 @@ import {buildQueryString} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; +import type ClientBase from './base'; + export interface ClientUsersMix { createUser: (user: UserProfile, token: string, inviteId: string) => Promise; patchMe: (userPatch: Partial) => Promise; @@ -46,9 +48,9 @@ export interface ClientUsersMix { removeRecentCustomStatus: (customStatus: UserCustomStatus) => Promise<{status: string}>; } -const ClientUsers = (superclass: any) => class extends superclass { +const ClientUsers = >(superclass: TBase) => class extends superclass { createUser = async (user: UserProfile, token: string, inviteId: string) => { - this.analytics.trackAPI('api_users_create'); + this.analytics?.trackAPI('api_users_create'); const queryParams: any = {}; @@ -74,7 +76,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; patchUser = async (userPatch: Partial & {id: string}) => { - this.analytics.trackAPI('api_users_patch'); + this.analytics?.trackAPI('api_users_patch'); return this.doFetch( `${this.getUserRoute(userPatch.id)}/patch`, @@ -83,7 +85,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; updateUser = async (user: UserProfile) => { - this.analytics.trackAPI('api_users_update'); + this.analytics?.trackAPI('api_users_update'); return this.doFetch( `${this.getUserRoute(user.id)}`, @@ -92,7 +94,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; demoteUserToGuest = async (userId: string) => { - this.analytics.trackAPI('api_users_demote_user_to_guest'); + this.analytics?.trackAPI('api_users_demote_user_to_guest'); return this.doFetch( `${this.getUserRoute(userId)}/demote`, @@ -101,7 +103,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getKnownUsers = async () => { - this.analytics.trackAPI('api_get_known_users'); + this.analytics?.trackAPI('api_get_known_users'); return this.doFetch( `${this.getUsersRoute()}/known`, @@ -110,7 +112,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; sendPasswordResetEmail = async (email: string) => { - this.analytics.trackAPI('api_users_send_password_reset'); + this.analytics?.trackAPI('api_users_send_password_reset'); return this.doFetch( `${this.getUsersRoute()}/password/reset/send`, @@ -119,7 +121,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; setDefaultProfileImage = async (userId: string) => { - this.analytics.trackAPI('api_users_set_default_profile_picture'); + this.analytics?.trackAPI('api_users_set_default_profile_picture'); return this.doFetch( `${this.getUserRoute(userId)}/image`, @@ -128,10 +130,10 @@ const ClientUsers = (superclass: any) => class extends superclass { }; login = async (loginId: string, password: string, token = '', deviceId = '', ldapOnly = false) => { - this.analytics.trackAPI('api_users_login'); + this.analytics?.trackAPI('api_users_login'); if (ldapOnly) { - this.analytics.trackAPI('api_users_login_ldap'); + this.analytics?.trackAPI('api_users_login_ldap'); } const body: any = { @@ -159,7 +161,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; loginById = async (id: string, password: string, token = '', deviceId = '') => { - this.analytics.trackAPI('api_users_login'); + this.analytics?.trackAPI('api_users_login'); const body: any = { device_id: deviceId, id, @@ -181,7 +183,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; logout = async () => { - this.analytics.trackAPI('api_users_logout'); + this.analytics?.trackAPI('api_users_logout'); const response = await this.doFetch( `${this.getUsersRoute()}/logout`, @@ -192,7 +194,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfiles = async (page = 0, perPage = PER_PAGE_DEFAULT, options = {}) => { - this.analytics.trackAPI('api_profiles_get'); + this.analytics?.trackAPI('api_profiles_get'); return this.doFetch( `${this.getUsersRoute()}${buildQueryString({page, per_page: perPage, ...options})}`, @@ -201,7 +203,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesByIds = async (userIds: string[], options = {}) => { - this.analytics.trackAPI('api_profiles_get_by_ids'); + this.analytics?.trackAPI('api_profiles_get_by_ids'); return this.doFetch( `${this.getUsersRoute()}/ids${buildQueryString(options)}`, @@ -210,7 +212,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesByUsernames = async (usernames: string[]) => { - this.analytics.trackAPI('api_profiles_get_by_usernames'); + this.analytics?.trackAPI('api_profiles_get_by_usernames'); return this.doFetch( `${this.getUsersRoute()}/usernames`, @@ -219,7 +221,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesInTeam = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT, sort = '', options = {}) => { - this.analytics.trackAPI('api_profiles_get_in_team', {team_id: teamId, sort}); + this.analytics?.trackAPI('api_profiles_get_in_team', {team_id: teamId, sort}); return this.doFetch( `${this.getUsersRoute()}${buildQueryString({...options, in_team: teamId, page, per_page: perPage, sort})}`, @@ -228,7 +230,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesNotInTeam = async (teamId: string, groupConstrained: boolean, page = 0, perPage = PER_PAGE_DEFAULT) => { - this.analytics.trackAPI('api_profiles_get_not_in_team', {team_id: teamId, group_constrained: groupConstrained}); + this.analytics?.trackAPI('api_profiles_get_not_in_team', {team_id: teamId, group_constrained: groupConstrained}); const queryStringObj: any = {not_in_team: teamId, page, per_page: perPage}; if (groupConstrained) { @@ -242,7 +244,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesWithoutTeam = async (page = 0, perPage = PER_PAGE_DEFAULT, options = {}) => { - this.analytics.trackAPI('api_profiles_get_without_team'); + this.analytics?.trackAPI('api_profiles_get_without_team'); return this.doFetch( `${this.getUsersRoute()}${buildQueryString({...options, without_team: 1, page, per_page: perPage})}`, @@ -251,7 +253,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesInChannel = async (channelId: string, options: GetUsersOptions) => { - this.analytics.trackAPI('api_profiles_get_in_channel', {channel_id: channelId}); + this.analytics?.trackAPI('api_profiles_get_in_channel', {channel_id: channelId}); const queryStringObj = {in_channel: channelId, ...options}; return this.doFetch( @@ -261,7 +263,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesInGroupChannels = async (channelsIds: string[]) => { - this.analytics.trackAPI('api_profiles_get_in_group_channels', {channelsIds}); + this.analytics?.trackAPI('api_profiles_get_in_group_channels', {channelsIds}); return this.doFetch( `${this.getUsersRoute()}/group_channels`, @@ -270,7 +272,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesNotInChannel = async (teamId: string, channelId: string, groupConstrained: boolean, page = 0, perPage = PER_PAGE_DEFAULT) => { - this.analytics.trackAPI('api_profiles_get_not_in_channel', {team_id: teamId, channel_id: channelId, group_constrained: groupConstrained}); + this.analytics?.trackAPI('api_profiles_get_not_in_channel', {team_id: teamId, channel_id: channelId, group_constrained: groupConstrained}); const queryStringObj: any = {in_team: teamId, not_in_channel: channelId, page, per_page: perPage}; if (groupConstrained) { @@ -372,7 +374,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; searchUsers = async (term: string, options: any) => { - this.analytics.trackAPI('api_search_users'); + this.analytics?.trackAPI('api_search_users'); return this.doFetch( `${this.getUsersRoute()}/search`, diff --git a/app/database/operator/app_data_operator/index.test.ts b/app/database/operator/app_data_operator/index.test.ts index 68edcfdd6..ae5870d6d 100644 --- a/app/database/operator/app_data_operator/index.test.ts +++ b/app/database/operator/app_data_operator/index.test.ts @@ -56,7 +56,7 @@ describe('** APP DATA OPERATOR **', () => { ], tableName: 'Info', prepareRecordsOnly: false, - }); + }, 'handleInfo'); }); it('=> HandleGlobal: should write to GLOBAL table', async () => { @@ -79,6 +79,6 @@ describe('** APP DATA OPERATOR **', () => { createOrUpdateRawValues: globals, tableName: 'Global', prepareRecordsOnly: false, - }); + }, 'handleGlobal'); }); }); diff --git a/app/database/operator/app_data_operator/index.ts b/app/database/operator/app_data_operator/index.ts index 5155603d8..da4896bd6 100644 --- a/app/database/operator/app_data_operator/index.ts +++ b/app/database/operator/app_data_operator/index.ts @@ -28,7 +28,7 @@ export default class AppDataOperator extends BaseDataOperator { prepareRecordsOnly, createOrUpdateRawValues: getUniqueRawsBy({raws: info, key: 'version_number'}), tableName: INFO, - }); + }, 'handleInfo'); }; handleGlobal = async ({globals, prepareRecordsOnly = true}: HandleGlobalArgs) => { @@ -45,6 +45,6 @@ export default class AppDataOperator extends BaseDataOperator { prepareRecordsOnly, createOrUpdateRawValues: getUniqueRawsBy({raws: globals, key: 'id'}), tableName: GLOBAL, - }); + }, 'handleGlobal'); }; } diff --git a/app/database/operator/base_data_operator/index.ts b/app/database/operator/base_data_operator/index.ts index 78bc9b749..5fa132318 100644 --- a/app/database/operator/base_data_operator/index.ts +++ b/app/database/operator/base_data_operator/index.ts @@ -22,10 +22,10 @@ import type { export interface BaseDataOperatorType { database: Database; - handleRecords: ({buildKeyRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues, tableName, prepareRecordsOnly}: HandleRecordsArgs) => Promise; - processRecords: ({createOrUpdateRawValues, deleteRawValues, tableName, buildKeyRecordBy, fieldName}: ProcessRecordsArgs) => Promise; - batchRecords: (models: Model[]) => Promise; - prepareRecords: ({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs) => Promise; + handleRecords: ({buildKeyRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues, tableName, prepareRecordsOnly}: HandleRecordsArgs, description: string) => Promise; + processRecords: ({createOrUpdateRawValues, deleteRawValues, tableName, buildKeyRecordBy, fieldName}: ProcessRecordsArgs) => Promise>; + batchRecords: (models: Model[], description: string) => Promise; + prepareRecords: ({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs) => Promise; } export default class BaseDataOperator { @@ -45,7 +45,7 @@ export default class BaseDataOperator { * @param {(existing: Model, newElement: RawValue) => boolean} inputsArg.buildKeyRecordBy * @returns {Promise<{ProcessRecordResults}>} */ - processRecords = async ({createOrUpdateRawValues = [], deleteRawValues = [], tableName, buildKeyRecordBy, fieldName}: ProcessRecordsArgs): Promise => { + processRecords = async ({createOrUpdateRawValues = [], deleteRawValues = [], tableName, buildKeyRecordBy, fieldName}: ProcessRecordsArgs): Promise> => { const getRecords = async (rawValues: RawValue[]) => { // We will query a table where one of its fields can match a range of values. Hence, here we are extracting all those potential values. const columnValues: string[] = getRangeOfValues({fieldName, raws: rawValues}); @@ -60,7 +60,7 @@ export default class BaseDataOperator { return []; } - const existingRecords = await retrieveRecords({ + const existingRecords = await retrieveRecords({ database: this.database, tableName, condition: Q.where(fieldName, Q.oneOf(columnValues)), @@ -125,13 +125,13 @@ export default class BaseDataOperator { * @param {(TransformerArgs) => Promise;} transformer * @returns {Promise} */ - prepareRecords = async ({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs): Promise => { + prepareRecords = async ({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs): Promise => { if (!this.database) { logWarning('Database not defined in prepareRecords'); return []; } - let preparedRecords: Array> = []; + let preparedRecords: Array> = []; // create operation if (createRaws?.length) { @@ -165,7 +165,7 @@ export default class BaseDataOperator { preparedRecords = preparedRecords.concat(recordPromises); } - const results = await Promise.all(preparedRecords); + const results = (await Promise.all(preparedRecords)) as T[]; if (deleteRaws?.length) { deleteRaws.forEach((deleteRecord) => { @@ -182,12 +182,12 @@ export default class BaseDataOperator { * @param {Array} models * @returns {Promise} */ - async batchRecords(models: Model[]): Promise { + async batchRecords(models: Model[], description: string): Promise { try { if (models.length > 0) { await this.database.write(async (writer) => { - await writer.batch(...models); - }); + await writer.batch(models); + }, description); } } catch (e) { logWarning('batchRecords error ', e as Error); @@ -205,7 +205,7 @@ export default class BaseDataOperator { * @param {string} handleRecordsArgs.tableName * @returns {Promise} */ - async handleRecords({buildKeyRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues = [], tableName, prepareRecordsOnly = true}: HandleRecordsArgs): Promise { + async handleRecords({buildKeyRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues = [], tableName, prepareRecordsOnly = true}: HandleRecordsArgs, description: string): Promise { if (!createOrUpdateRawValues.length) { logWarning( `An empty "rawValues" array has been passed to the handleRecords method for tableName ${tableName}`, @@ -213,7 +213,7 @@ export default class BaseDataOperator { return []; } - const {createRaws, deleteRaws, updateRaws} = await this.processRecords({ + const {createRaws, deleteRaws, updateRaws} = await this.processRecords({ createOrUpdateRawValues, deleteRawValues, tableName, @@ -221,8 +221,8 @@ export default class BaseDataOperator { fieldName, }); - let models: Model[] = []; - models = await this.prepareRecords({ + let models: T[] = []; + models = await this.prepareRecords({ tableName, createRaws, updateRaws, @@ -231,7 +231,7 @@ export default class BaseDataOperator { }); if (!prepareRecordsOnly && models?.length) { - await this.batchRecords(models); + await this.batchRecords(models, description); } return models; diff --git a/app/database/operator/server_data_operator/handlers/category.test.ts b/app/database/operator/server_data_operator/handlers/category.test.ts index b2188aef3..7a534551c 100644 --- a/app/database/operator/server_data_operator/handlers/category.test.ts +++ b/app/database/operator/server_data_operator/handlers/category.test.ts @@ -46,7 +46,7 @@ describe('*** Operator: Category Handlers tests ***', () => { tableName: MM_TABLES.SERVER.CATEGORY, prepareRecordsOnly: false, transformer: transformCategoryRecord, - }); + }, 'handleCategories'); }); it('=> handleCategoryChannels: should write to the CATEGORY_CHANNEL table', async () => { @@ -74,6 +74,6 @@ describe('*** Operator: Category Handlers tests ***', () => { tableName: MM_TABLES.SERVER.CATEGORY_CHANNEL, prepareRecordsOnly: false, transformer: transformCategoryChannelRecord, - }); + }, 'handleCategoryChannels'); }); }); diff --git a/app/database/operator/server_data_operator/handlers/category.ts b/app/database/operator/server_data_operator/handlers/category.ts index fb1e94858..853193f38 100644 --- a/app/database/operator/server_data_operator/handlers/category.ts +++ b/app/database/operator/server_data_operator/handlers/category.ts @@ -11,6 +11,7 @@ import { import {getUniqueRawsBy} from '@database/operator/utils/general'; import {logWarning} from '@utils/log'; +import type ServerDataOperatorBase from '.'; import type { HandleCategoryChannelArgs, HandleCategoryArgs, @@ -28,7 +29,7 @@ export interface CategoryHandlerMix { handleCategories: ({categories, prepareRecordsOnly}: HandleCategoryArgs) => Promise; } -const CategoryHandler = (superclass: any) => class extends superclass { +const CategoryHandler = >(superclass: TBase) => class extends superclass { /** * handleCategories: Handler responsible for the Create/Update operations occurring on the Category table from the 'Server' schema * @param {HandleCategoryArgs} categoriesArgs @@ -78,7 +79,7 @@ const CategoryHandler = (superclass: any) => class extends superclass { createOrUpdateRawValues, tableName: CATEGORY, prepareRecordsOnly, - }); + }, 'handleCategories'); }; /** @@ -88,7 +89,7 @@ const CategoryHandler = (superclass: any) => class extends superclass { * @param {boolean} categoriesArgs.prepareRecordsOnly * @returns {Promise} */ - handleCategoryChannels = async ({categoryChannels, prepareRecordsOnly = true}: HandleCategoryChannelArgs): Promise => { + handleCategoryChannels = async ({categoryChannels, prepareRecordsOnly = true}: HandleCategoryChannelArgs): Promise => { if (!categoryChannels?.length) { logWarning( 'An empty or undefined "categoryChannels" array has been passed to the handleCategories method', @@ -105,7 +106,7 @@ const CategoryHandler = (superclass: any) => class extends superclass { createOrUpdateRawValues, tableName: CATEGORY_CHANNEL, prepareRecordsOnly, - }); + }, 'handleCategoryChannels'); }; }; diff --git a/app/database/operator/server_data_operator/handlers/channel.test.ts b/app/database/operator/server_data_operator/handlers/channel.test.ts index d3fe04247..d5c3b5df8 100644 --- a/app/database/operator/server_data_operator/handlers/channel.test.ts +++ b/app/database/operator/server_data_operator/handlers/channel.test.ts @@ -63,7 +63,7 @@ describe('*** Operator: Channel Handlers tests ***', () => { tableName: 'Channel', prepareRecordsOnly: false, transformer: transformChannelRecord, - }); + }, 'handleChannel'); }); it('=> HandleMyChannelSettings: should write to the MY_CHANNEL_SETTINGS table', async () => { @@ -103,7 +103,7 @@ describe('*** Operator: Channel Handlers tests ***', () => { prepareRecordsOnly: false, buildKeyRecordBy: buildMyChannelKey, transformer: transformMyChannelSettingsRecord, - }); + }, 'handleMyChannelSettings'); }); it('=> HandleChannelInfo: should write to the CHANNEL_INFO table', async () => { @@ -134,7 +134,7 @@ describe('*** Operator: Channel Handlers tests ***', () => { tableName: 'ChannelInfo', prepareRecordsOnly: false, transformer: transformChannelInfoRecord, - }); + }, 'handleChannelInfo'); }); it('=> HandleMyChannel: should write to the MY_CHANNEL table', async () => { @@ -195,7 +195,7 @@ describe('*** Operator: Channel Handlers tests ***', () => { prepareRecordsOnly: false, buildKeyRecordBy: buildMyChannelKey, transformer: transformMyChannelRecord, - }); + }, 'handleMyChannel'); }); it('=> HandleMyChannel: should keep the previous lastFetchedAt for MY_CHANNEL', async () => { @@ -314,6 +314,6 @@ describe('*** Operator: Channel Handlers tests ***', () => { prepareRecordsOnly: false, buildKeyRecordBy: buildChannelMembershipKey, transformer: transformChannelMembershipRecord, - }); + }, 'handleChannelMembership'); }); }); diff --git a/app/database/operator/server_data_operator/handlers/channel.ts b/app/database/operator/server_data_operator/handlers/channel.ts index cb741a084..942e84820 100644 --- a/app/database/operator/server_data_operator/handlers/channel.ts +++ b/app/database/operator/server_data_operator/handlers/channel.ts @@ -19,6 +19,7 @@ import {getUniqueRawsBy} from '@database/operator/utils/general'; import {getIsCRTEnabled} from '@queries/servers/thread'; import {logWarning} from '@utils/log'; +import type ServerDataOperatorBase from '.'; import type {HandleChannelArgs, HandleChannelInfoArgs, HandleChannelMembershipArgs, HandleMyChannelArgs, HandleMyChannelSettingsArgs} from '@typings/database/database'; import type ChannelModel from '@typings/database/models/servers/channel'; import type ChannelInfoModel from '@typings/database/models/servers/channel_info'; @@ -42,7 +43,7 @@ export interface ChannelHandlerMix { handleMyChannel: ({channels, myChannels, isCRTEnabled, prepareRecordsOnly}: HandleMyChannelArgs) => Promise; } -const ChannelHandler = (superclass: any) => class extends superclass { +const ChannelHandler = >(superclass: TBase) => class extends superclass { /** * handleChannel: Handler responsible for the Create/Update operations occurring on the CHANNEL table from the 'Server' schema * @param {HandleChannelArgs} channelsArgs @@ -89,7 +90,7 @@ const ChannelHandler = (superclass: any) => class extends superclass { prepareRecordsOnly, createOrUpdateRawValues, tableName: CHANNEL, - }); + }, 'handleChannel'); }; /** @@ -146,7 +147,7 @@ const ChannelHandler = (superclass: any) => class extends superclass { prepareRecordsOnly, createOrUpdateRawValues, tableName: MY_CHANNEL_SETTINGS, - }); + }, 'handleMyChannelSettings'); }; /** @@ -205,7 +206,7 @@ const ChannelHandler = (superclass: any) => class extends superclass { prepareRecordsOnly, createOrUpdateRawValues, tableName: CHANNEL_INFO, - }); + }, 'handleChannelInfo'); }; /** @@ -292,7 +293,7 @@ const ChannelHandler = (superclass: any) => class extends superclass { prepareRecordsOnly, createOrUpdateRawValues, tableName: MY_CHANNEL, - }); + }, 'handleMyChannel'); }; /** @@ -348,7 +349,7 @@ const ChannelHandler = (superclass: any) => class extends superclass { prepareRecordsOnly, createOrUpdateRawValues, tableName: CHANNEL_MEMBERSHIP, - }); + }, 'handleChannelMembership'); }; }; diff --git a/app/database/operator/server_data_operator/handlers/group.test.ts b/app/database/operator/server_data_operator/handlers/group.test.ts index 30cf6c203..2264c6e7c 100644 --- a/app/database/operator/server_data_operator/handlers/group.test.ts +++ b/app/database/operator/server_data_operator/handlers/group.test.ts @@ -49,6 +49,6 @@ describe('*** Operator: Group Handlers tests ***', () => { tableName: MM_TABLES.SERVER.GROUP, prepareRecordsOnly: false, transformer: transformGroupRecord, - }); + }, 'handleGroups'); }); }); diff --git a/app/database/operator/server_data_operator/handlers/group.ts b/app/database/operator/server_data_operator/handlers/group.ts index 0a64dd2e7..73050bd12 100644 --- a/app/database/operator/server_data_operator/handlers/group.ts +++ b/app/database/operator/server_data_operator/handlers/group.ts @@ -8,6 +8,7 @@ import {queryGroupChannelForChannel, queryGroupMembershipForMember, queryGroupTe import {generateGroupAssociationId} from '@utils/groups'; import {logWarning} from '@utils/log'; +import type ServerDataOperatorBase from '.'; import type {HandleGroupArgs, HandleGroupChannelsForChannelArgs, HandleGroupMembershipForMemberArgs, HandleGroupTeamsForTeamArgs} from '@typings/database/database'; import type GroupModel from '@typings/database/models/servers/group'; import type GroupChannelModel from '@typings/database/models/servers/group_channel'; @@ -23,7 +24,7 @@ export interface GroupHandlerMix { handleGroupTeamsForTeam: ({teamId, groups, prepareRecordsOnly}: HandleGroupTeamsForTeamArgs) => Promise; } -const GroupHandler = (superclass: any) => class extends superclass implements GroupHandlerMix { +const GroupHandler = >(superclass: TBase) => class extends superclass implements GroupHandlerMix { /** * handleGroups: Handler responsible for the Create/Update operations occurring on the Group table from the 'Server' schema * @@ -46,7 +47,7 @@ const GroupHandler = (superclass: any) => class extends superclass implements Gr createOrUpdateRawValues, tableName: GROUP, prepareRecordsOnly, - }); + }, 'handleGroups'); }; /** @@ -93,14 +94,14 @@ const GroupHandler = (superclass: any) => class extends superclass implements Gr records.push(...(await this.handleRecords({ fieldName: 'id', transformer: transformGroupChannelRecord, - rawValues, + createOrUpdateRawValues: rawValues, tableName: GROUP_CHANNEL, prepareRecordsOnly: true, - }))); + }, 'handleGroupChannelsForChannel'))); // Batch update if there are records if (records.length && !prepareRecordsOnly) { - await this.batchRecords(records); + await this.batchRecords(records, 'handleGroupChannelsForChannel'); } return records; @@ -150,14 +151,14 @@ const GroupHandler = (superclass: any) => class extends superclass implements Gr records.push(...(await this.handleRecords({ fieldName: 'id', transformer: transformGroupMembershipRecord, - rawValues, + createOrUpdateRawValues: rawValues, tableName: GROUP_MEMBERSHIP, prepareRecordsOnly: true, - }))); + }, 'handleGroupMembershipsForMember'))); // Batch update if there are records if (records.length && !prepareRecordsOnly) { - await this.batchRecords(records); + await this.batchRecords(records, 'handleGroupMembershipsForMember'); } return records; @@ -207,14 +208,14 @@ const GroupHandler = (superclass: any) => class extends superclass implements Gr records.push(...(await this.handleRecords({ fieldName: 'id', transformer: transformGroupTeamRecord, - rawValues, + createOrUpdateRawValues: rawValues, tableName: GROUP_TEAM, prepareRecordsOnly: true, - }))); + }, 'handleGroupTeamsForTeam'))); // Batch update if there are records if (records.length && !prepareRecordsOnly) { - await this.batchRecords(records); + await this.batchRecords(records, 'handleGroupTeamsForTeam'); } return records; diff --git a/app/database/operator/server_data_operator/handlers/index.test.ts b/app/database/operator/server_data_operator/handlers/index.test.ts index d7330dc87..9ce3d4744 100644 --- a/app/database/operator/server_data_operator/handlers/index.test.ts +++ b/app/database/operator/server_data_operator/handlers/index.test.ts @@ -42,7 +42,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => { createOrUpdateRawValues: roles, tableName: 'Role', prepareRecordsOnly: false, - }); + }, 'handleRole'); }); it('=> HandleCustomEmojis: should write to the CUSTOM_EMOJI table', async () => { @@ -72,7 +72,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => { tableName: 'CustomEmoji', prepareRecordsOnly: false, transformer: transformCustomEmojiRecord, - }); + }, 'handleCustomEmojis'); }); it('=> HandleSystem: should write to the SYSTEM table', async () => { @@ -93,7 +93,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => { createOrUpdateRawValues: systems, tableName: 'System', prepareRecordsOnly: false, - }); + }, 'handleSystem'); }); it('=> HandleConfig: should write to the CONFIG table', async () => { @@ -117,7 +117,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => { tableName: 'Config', prepareRecordsOnly: false, deleteRawValues: configsToDelete, - }); + }, 'handleConfigs'); }); it('=> No table name: should not call execute if tableName is invalid', async () => { @@ -135,7 +135,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => { transformer: transformSystemRecord, createOrUpdateRawValues: [{id: 'tos-1', value: '1'}], prepareRecordsOnly: false, - }), + }, 'test'), ).rejects.toThrow(Error); }); }); diff --git a/app/database/operator/server_data_operator/handlers/index.ts b/app/database/operator/server_data_operator/handlers/index.ts index b3a5a9356..15ce6f282 100644 --- a/app/database/operator/server_data_operator/handlers/index.ts +++ b/app/database/operator/server_data_operator/handlers/index.ts @@ -12,13 +12,17 @@ import { import {getUniqueRawsBy} from '@database/operator/utils/general'; import {logWarning} from '@utils/log'; +import {sanitizeReactions} from '../../utils/reaction'; +import {transformReactionRecord} from '../transformers/reaction'; + import type {Model} from '@nozbe/watermelondb'; -import type {HandleConfigArgs, HandleCustomEmojiArgs, HandleRoleArgs, HandleSystemArgs, OperationArgs} from '@typings/database/database'; +import type {HandleConfigArgs, HandleCustomEmojiArgs, HandleReactionsArgs, HandleRoleArgs, HandleSystemArgs, OperationArgs} from '@typings/database/database'; import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji'; +import type ReactionModel from '@typings/database/models/servers/reaction'; import type RoleModel from '@typings/database/models/servers/role'; import type SystemModel from '@typings/database/models/servers/system'; -const {SERVER: {CONFIG, CUSTOM_EMOJI, ROLE, SYSTEM}} = MM_TABLES; +const {SERVER: {CONFIG, CUSTOM_EMOJI, ROLE, SYSTEM, REACTION}} = MM_TABLES; export default class ServerDataOperatorBase extends BaseDataOperator { handleRole = async ({roles, prepareRecordsOnly = true}: HandleRoleArgs) => { @@ -35,7 +39,7 @@ export default class ServerDataOperatorBase extends BaseDataOperator { prepareRecordsOnly, createOrUpdateRawValues: getUniqueRawsBy({raws: roles, key: 'id'}), tableName: ROLE, - }) as Promise; + }, 'handleRole') as Promise; }; handleCustomEmojis = async ({emojis, prepareRecordsOnly = true}: HandleCustomEmojiArgs) => { @@ -52,7 +56,7 @@ export default class ServerDataOperatorBase extends BaseDataOperator { prepareRecordsOnly, createOrUpdateRawValues: getUniqueRawsBy({raws: emojis, key: 'name'}), tableName: CUSTOM_EMOJI, - }) as Promise; + }, 'handleCustomEmojis') as Promise; }; handleSystem = async ({systems, prepareRecordsOnly = true}: HandleSystemArgs) => { @@ -69,7 +73,7 @@ export default class ServerDataOperatorBase extends BaseDataOperator { prepareRecordsOnly, createOrUpdateRawValues: getUniqueRawsBy({raws: systems, key: 'id'}), tableName: SYSTEM, - }) as Promise; + }, 'handleSystem') as Promise; }; handleConfigs = async ({configs, configsToDelete, prepareRecordsOnly = true}: HandleConfigArgs) => { @@ -87,7 +91,64 @@ export default class ServerDataOperatorBase extends BaseDataOperator { createOrUpdateRawValues: getUniqueRawsBy({raws: configs, key: 'id'}), tableName: CONFIG, deleteRawValues: configsToDelete, - }); + }, 'handleConfigs'); + }; + + /** + * handleReactions: Handler responsible for the Create/Update operations occurring on the Reaction table from the 'Server' schema + * @param {HandleReactionsArgs} handleReactions + * @param {ReactionsPerPost[]} handleReactions.postsReactions + * @param {boolean} handleReactions.prepareRecordsOnly + * @param {boolean} handleReactions.skipSync + * @returns {Promise>} + */ + handleReactions = async ({postsReactions, prepareRecordsOnly, skipSync}: HandleReactionsArgs): Promise => { + const batchRecords: ReactionModel[] = []; + + if (!postsReactions?.length) { + logWarning( + 'An empty or undefined "postsReactions" array has been passed to the handleReactions method', + ); + return []; + } + + for await (const postReactions of postsReactions) { + const {post_id, reactions} = postReactions; + const { + createReactions, + deleteReactions, + } = await sanitizeReactions({ + database: this.database, + post_id, + rawReactions: reactions, + skipSync, + }); + + if (createReactions?.length) { + // Prepares record for model Reactions + const reactionsRecords = (await this.prepareRecords({ + createRaws: createReactions, + transformer: transformReactionRecord, + tableName: REACTION, + })) as ReactionModel[]; + batchRecords.push(...reactionsRecords); + } + + if (deleteReactions?.length && !skipSync) { + deleteReactions.forEach((outCast) => outCast.prepareDestroyPermanently()); + batchRecords.push(...deleteReactions); + } + } + + if (prepareRecordsOnly) { + return batchRecords; + } + + if (batchRecords?.length) { + await this.batchRecords(batchRecords, 'handleReactions'); + } + + return batchRecords; }; /** @@ -99,7 +160,7 @@ export default class ServerDataOperatorBase extends BaseDataOperator { * @param {(TransformerArgs) => Promise} execute.recordOperator * @returns {Promise} */ - async execute({createRaws, transformer, tableName, updateRaws}: OperationArgs): Promise { + async execute({createRaws, transformer, tableName, updateRaws}: OperationArgs, description: string): Promise { const models = await this.prepareRecords({ tableName, createRaws, @@ -108,7 +169,7 @@ export default class ServerDataOperatorBase extends BaseDataOperator { }); if (models?.length > 0) { - await this.batchRecords(models); + await this.batchRecords(models, description); } return models; diff --git a/app/database/operator/server_data_operator/handlers/post.test.ts b/app/database/operator/server_data_operator/handlers/post.test.ts index 2d08b3b18..d91089c2b 100644 --- a/app/database/operator/server_data_operator/handlers/post.test.ts +++ b/app/database/operator/server_data_operator/handlers/post.test.ts @@ -61,7 +61,7 @@ describe('*** Operator: Post Handlers tests ***', () => { createOrUpdateRawValues: drafts, tableName: 'Draft', prepareRecordsOnly: false, - }); + }, 'handleDraft'); }); it('=> HandlePosts: should write to the Post and its sub-child tables', async () => { diff --git a/app/database/operator/server_data_operator/handlers/post.ts b/app/database/operator/server_data_operator/handlers/post.ts index d32da053e..83e3c11aa 100644 --- a/app/database/operator/server_data_operator/handlers/post.ts +++ b/app/database/operator/server_data_operator/handlers/post.ts @@ -9,15 +9,19 @@ import {buildDraftKey} from '@database/operator/server_data_operator/comparators import { transformDraftRecord, transformFileRecord, + transformPostInThreadRecord, transformPostRecord, + transformPostsInChannelRecord, } from '@database/operator/server_data_operator/transformers/post'; -import {getUniqueRawsBy} from '@database/operator/utils/general'; -import {createPostsChain} from '@database/operator/utils/post'; +import {getRawRecordPairs, getUniqueRawsBy, getValidRecordsForUpdate} from '@database/operator/utils/general'; +import {createPostsChain, getPostListEdges} from '@database/operator/utils/post'; +import {emptyFunction} from '@utils/general'; import {logWarning} from '@utils/log'; +import type ServerDataOperatorBase from '.'; import type Database from '@nozbe/watermelondb/Database'; import type Model from '@nozbe/watermelondb/Model'; -import type {HandleDraftArgs, HandleFilesArgs, HandlePostsArgs, ProcessRecordResults} from '@typings/database/database'; +import type {HandleDraftArgs, HandleFilesArgs, HandlePostsArgs, RecordPair} from '@typings/database/database'; import type DraftModel from '@typings/database/models/servers/draft'; import type FileModel from '@typings/database/models/servers/file'; import type PostModel from '@typings/database/models/servers/post'; @@ -29,6 +33,8 @@ const { DRAFT, FILE, POST, + POSTS_IN_CHANNEL, + POSTS_IN_THREAD, } = MM_TABLES.SERVER; type PostActionType = keyof typeof ActionType.POSTS; @@ -39,9 +45,17 @@ export interface PostHandlerMix { handlePosts: ({actionType, order, posts, previousPostId, prepareRecordsOnly}: HandlePostsArgs) => Promise; handlePostsInChannel: (posts: Post[]) => Promise; handlePostsInThread: (rootPosts: PostsInThread[]) => Promise; + + handleReceivedPostsInChannel: (posts: Post[], prepareRecordsOnly?: boolean) => Promise; + handleReceivedPostsInChannelSince: (posts: Post[], prepareRecordsOnly?: boolean) => Promise; + handleReceivedPostsInChannelBefore: (posts: Post[], prepareRecordsOnly?: boolean) => Promise; + handleReceivedPostsInChannelAfter: (posts: Post[], prepareRecordsOnly?: boolean) => Promise; + handleReceivedPostForChannel: (post: Post, prepareRecordsOnly?: boolean) => Promise; + + handleReceivedPostsInThread: (postsMap: Record, prepareRecordsOnly?: boolean) => Promise; } -const PostHandler = (superclass: any) => class extends superclass { +const PostHandler = >(superclass: TBase) => class extends superclass { /** * handleDraft: Handler responsible for the Create/Update operations occurring the Draft table from the 'Server' schema * @param {HandleDraftArgs} draftsArgs @@ -66,7 +80,7 @@ const PostHandler = (superclass: any) => class extends superclass { prepareRecordsOnly, createOrUpdateRawValues, tableName: DRAFT, - }); + }, 'handleDraft'); }; /** @@ -171,7 +185,7 @@ const PostHandler = (superclass: any) => class extends superclass { deleteRawValues: pendingPostsToDelete, tableName, fieldName: 'id', - })) as ProcessRecordResults; + })); const preparedPosts = (await this.prepareRecords({ createRaws: processedPosts.createRaws, @@ -220,7 +234,7 @@ const PostHandler = (superclass: any) => class extends superclass { } if (batch.length && !prepareRecordsOnly) { - await this.batchRecords(batch); + await this.batchRecords(batch, 'handlePosts'); } return batch; @@ -245,7 +259,8 @@ const PostHandler = (superclass: any) => class extends superclass { createOrUpdateRawValues: files, tableName: FILE, fieldName: 'id', - })) as ProcessRecordResults; + deleteRawValues: [], + })); const postFiles = await this.prepareRecords({ createRaws: processedFiles.createRaws, @@ -259,7 +274,7 @@ const PostHandler = (superclass: any) => class extends superclass { } if (postFiles?.length) { - await this.batchRecords(postFiles); + await this.batchRecords(postFiles, 'handleFiles'); } return postFiles; @@ -327,6 +342,340 @@ const PostHandler = (superclass: any) => class extends superclass { return []; }; + + // ======================== + // POST IN CHANNEL + // ======================== + + _createPostsInChannelRecord = (channelId: string, earliest: number, latest: number, prepareRecordsOnly = false): Promise => { + // We should prepare instead of execute + if (prepareRecordsOnly) { + return this.prepareRecords({ + tableName: POSTS_IN_CHANNEL, + createRaws: [{record: undefined, raw: {channel_id: channelId, earliest, latest}}], + transformer: transformPostsInChannelRecord, + }); + } + return this.execute({ + createRaws: [{record: undefined, raw: {channel_id: channelId, earliest, latest}}], + tableName: POSTS_IN_CHANNEL, + transformer: transformPostsInChannelRecord, + }, '_createPostsInChannelRecord'); + }; + + _mergePostInChannelChunks = async (newChunk: PostsInChannelModel, existingChunks: PostsInChannelModel[], prepareRecordsOnly = false) => { + const result: PostsInChannelModel[] = []; + for (const chunk of existingChunks) { + if (newChunk.earliest <= chunk.earliest && newChunk.latest >= chunk.latest) { + if (!prepareRecordsOnly) { + newChunk.prepareUpdate(emptyFunction); + } + result.push(newChunk); + result.push(chunk.prepareDestroyPermanently()); + break; + } + } + + if (result.length && !prepareRecordsOnly) { + await this.batchRecords(result, '_mergePostInChannelChunks'); + } + + return result; + }; + + handleReceivedPostsInChannel = async (posts?: Post[], prepareRecordsOnly = false): Promise => { + if (!posts?.length) { + logWarning( + 'An empty or undefined "posts" array has been passed to the handleReceivedPostsInChannel method', + ); + return []; + } + + const {firstPost, lastPost} = getPostListEdges(posts); + + // Channel Id for this chain of posts + const channelId = firstPost.channel_id; + + // Find smallest 'create_at' value in chain + const earliest = firstPost.create_at; + + // Find highest 'create_at' value in chain; -1 means we are dealing with one item in the posts array + const latest = lastPost.create_at; + + // Find the records in the PostsInChannel table that have a matching channel_id + const chunks = (await this.database.get(POSTS_IN_CHANNEL).query( + Q.where('channel_id', channelId), + Q.sortBy('latest', Q.desc), + ).fetch()) as PostsInChannelModel[]; + + // chunk length 0; then it's a new chunk to be added to the PostsInChannel table + if (chunks.length === 0) { + return this._createPostsInChannelRecord(channelId, earliest, latest, prepareRecordsOnly); + } + + let targetChunk: PostsInChannelModel|undefined; + + for (const chunk of chunks) { + // find if we should plug the chain before + if (firstPost.create_at >= chunk.earliest || latest <= chunk.latest) { + targetChunk = chunk; + break; + } + } + + if (targetChunk) { + if ( + targetChunk.earliest <= earliest && + targetChunk.latest >= latest + ) { + return []; + } + + // If the chunk was found, Update the chunk and return + if (prepareRecordsOnly) { + targetChunk.prepareUpdate((record) => { + record.earliest = Math.min(record.earliest, earliest); + record.latest = Math.max(record.latest, latest); + }); + return [targetChunk]; + } + + targetChunk = await this.database.write(async () => { + return targetChunk!.update((record) => { + record.earliest = Math.min(record.earliest, earliest); + record.latest = Math.max(record.latest, latest); + }); + }); + + return [targetChunk!]; + } + + // Create a new chunk and merge them if needed + const newChunk = await this._createPostsInChannelRecord(channelId, earliest, latest, prepareRecordsOnly); + const merged = await this._mergePostInChannelChunks(newChunk[0], chunks, prepareRecordsOnly); + return merged; + }; + + handleReceivedPostsInChannelSince = async (posts: Post[], prepareRecordsOnly = false): Promise => { + if (!posts?.length) { + logWarning( + 'An empty or undefined "posts" array has been passed to the handleReceivedPostsInChannelSince method', + ); + return []; + } + + const {firstPost} = getPostListEdges(posts); + let latest = 0; + + let recentChunk: PostsInChannelModel|undefined; + const chunks = (await this.database.get(POSTS_IN_CHANNEL).query( + Q.where('channel_id', firstPost.channel_id), + Q.sortBy('latest', Q.desc), + ).fetch()) as PostsInChannelModel[]; + + if (chunks.length) { + recentChunk = chunks[0]; + + // add any new recent post while skipping the ones that were just updated + for (const post of posts) { + if (post.create_at > recentChunk.latest) { + latest = post.create_at; + } + } + } + + if (recentChunk && recentChunk.latest < latest) { + // We've got new posts that belong to this chunk + if (prepareRecordsOnly) { + recentChunk.prepareUpdate((record) => { + record.latest = Math.max(record.latest, latest); + }); + + return [recentChunk]; + } + + recentChunk = await this.database.write(async () => { + return recentChunk!.update((record) => { + record.latest = Math.max(record.latest, latest); + }); + }); + + return [recentChunk!]; + } + + return []; + }; + + handleReceivedPostsInChannelBefore = async (posts: Post[], prepareRecordsOnly = false): Promise => { + if (!posts?.length) { + logWarning( + 'An empty or undefined "posts" array has been passed to the handleReceivedPostsInChannelBefore method', + ); + return []; + } + + const {firstPost} = getPostListEdges(posts); + + // Channel Id for this chain of posts + const channelId = firstPost.channel_id; + + // Find smallest 'create_at' value in chain + const earliest = firstPost.create_at; + + // Find the records in the PostsInChannel table that have a matching channel_id + const chunks = (await this.database.get(POSTS_IN_CHANNEL).query( + Q.where('channel_id', channelId), + Q.sortBy('latest', Q.desc), + ).fetch()) as PostsInChannelModel[]; + + if (chunks.length === 0) { + // No chunks found, previous posts in this block not found + return []; + } + + let targetChunk = chunks[0]; + if (targetChunk) { + if (targetChunk.earliest <= earliest) { + return []; + } + + // If the chunk was found, Update the chunk and return + if (prepareRecordsOnly) { + targetChunk.prepareUpdate((record) => { + record.earliest = Math.min(record.earliest, earliest); + }); + return [targetChunk]; + } + + targetChunk = await this.database.write(async () => { + return targetChunk!.update((record) => { + record.earliest = Math.min(record.earliest, earliest); + }); + }); + + return [targetChunk!]; + } + + return targetChunk; + }; + + handleReceivedPostsInChannelAfter = async (posts: Post[], prepareRecordsOnly = false): Promise => { + throw new Error(`handleReceivedPostsInChannelAfter Not implemented yet. posts count${posts.length} prepareRecordsOnly=${prepareRecordsOnly}`); + }; + + handleReceivedPostForChannel = async (posts: Post[], prepareRecordsOnly = false): Promise => { + if (!posts?.length) { + logWarning( + 'An empty or undefined "posts" array has been passed to the handleReceivedPostForChannel method', + ); + return []; + } + + const {firstPost, lastPost} = getPostListEdges(posts); + + // Channel Id for this chain of posts + const channelId = firstPost.channel_id; + + // Find smallest 'create_at' value in chain + const earliest = firstPost.create_at; + + // Find highest 'create_at' value in chain; -1 means we are dealing with one item in the posts array + const latest = lastPost.create_at; + + // Find the records in the PostsInChannel table that have a matching channel_id + const chunks = (await this.database.get(POSTS_IN_CHANNEL).query( + Q.where('channel_id', channelId), + Q.sortBy('latest', Q.desc), + ).fetch()) as PostsInChannelModel[]; + + // chunk length 0; then it's a new chunk to be added to the PostsInChannel table + if (chunks.length === 0) { + return this._createPostsInChannelRecord(channelId, earliest, latest, prepareRecordsOnly); + } + + let targetChunk = chunks[0]; + if (targetChunk) { + if (targetChunk.latest >= latest) { + return []; + } + + // If the chunk was found, Update the chunk and return + if (prepareRecordsOnly) { + targetChunk.prepareUpdate((record) => { + record.latest = Math.max(record.latest, latest); + }); + return [targetChunk]; + } + + targetChunk = await this.database.write(async () => { + return targetChunk!.update((record) => { + record.latest = Math.max(record.latest, latest); + }); + }); + + return [targetChunk!]; + } + + return targetChunk; + }; + + // ======================== + // POST IN THREAD + // ======================== + + handleReceivedPostsInThread = async (postsMap: Record, prepareRecordsOnly = false): Promise => { + if (!postsMap || !Object.keys(postsMap).length) { + logWarning( + 'An empty or undefined "postsMap" object has been passed to the handleReceivedPostsInThread method', + ); + return []; + } + + const update: RecordPair[] = []; + const create: PostsInThread[] = []; + const ids = Object.keys(postsMap); + for await (const rootId of ids) { + const {firstPost, lastPost} = getPostListEdges(postsMap[rootId]); + const chunks = (await this.database.get(POSTS_IN_THREAD).query( + Q.where('root_id', rootId), + Q.sortBy('latest', Q.desc), + ).fetch()) as PostsInThreadModel[]; + + if (chunks.length) { + const chunk = chunks[0]; + const newValue = { + root_id: rootId, + earliest: Math.min(chunk.earliest, firstPost.create_at), + latest: Math.max(chunk.latest, lastPost.create_at), + }; + update.push(getValidRecordsForUpdate({ + tableName: POSTS_IN_THREAD, + newValue, + existingRecord: chunk, + })); + } else { + // create chunk + create.push({ + root_id: rootId, + earliest: firstPost.create_at, + latest: lastPost.create_at, + }); + } + } + + const postInThreadRecords = (await this.prepareRecords({ + createRaws: getRawRecordPairs(create), + updateRaws: update, + transformer: transformPostInThreadRecord, + tableName: POSTS_IN_THREAD, + })) as PostsInThreadModel[]; + + if (postInThreadRecords?.length && !prepareRecordsOnly) { + await this.batchRecords(postInThreadRecords, 'handleReceivedPostsInThread'); + } + + return postInThreadRecords; + }; }; export default PostHandler; diff --git a/app/database/operator/server_data_operator/handlers/posts_in_channel.ts b/app/database/operator/server_data_operator/handlers/posts_in_channel.ts deleted file mode 100644 index 6bb8f8040..000000000 --- a/app/database/operator/server_data_operator/handlers/posts_in_channel.ts +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {Q} from '@nozbe/watermelondb'; - -import {Database} from '@constants'; -import {getPostListEdges} from '@database//operator/utils/post'; -import {transformPostsInChannelRecord} from '@database/operator/server_data_operator/transformers/post'; -import {emptyFunction} from '@utils/general'; -import {logWarning} from '@utils/log'; - -import type PostsInChannelModel from '@typings/database/models/servers/posts_in_channel'; - -export interface PostsInChannelHandlerMix { - handleReceivedPostsInChannel: (posts: Post[], prepareRecordsOnly?: boolean) => Promise; - handleReceivedPostsInChannelSince: (posts: Post[], prepareRecordsOnly?: boolean) => Promise; - handleReceivedPostsInChannelBefore: (posts: Post[], prepareRecordsOnly?: boolean) => Promise; - handleReceivedPostsInChannelAfter: (posts: Post[], prepareRecordsOnly?: boolean) => Promise; - handleReceivedPostForChannel: (post: Post, prepareRecordsOnly?: boolean) => Promise; -} - -const {POSTS_IN_CHANNEL} = Database.MM_TABLES.SERVER; - -const PostsInChannelHandler = (superclass: any) => class extends superclass { - _createPostsInChannelRecord = (channelId: string, earliest: number, latest: number, prepareRecordsOnly = false): Promise => { - // We should prepare instead of execute - if (prepareRecordsOnly) { - return this.prepareRecords({ - tableName: POSTS_IN_CHANNEL, - createRaws: [{record: undefined, raw: {channel_id: channelId, earliest, latest}}], - transformer: transformPostsInChannelRecord, - }); - } - return this.execute({ - createRaws: [{record: undefined, raw: {channel_id: channelId, earliest, latest}}], - tableName: POSTS_IN_CHANNEL, - transformer: transformPostsInChannelRecord, - }); - }; - - _mergePostInChannelChunks = async (newChunk: PostsInChannelModel, existingChunks: PostsInChannelModel[], prepareRecordsOnly = false) => { - const result: PostsInChannelModel[] = []; - for (const chunk of existingChunks) { - if (newChunk.earliest <= chunk.earliest && newChunk.latest >= chunk.latest) { - if (!prepareRecordsOnly) { - newChunk.prepareUpdate(emptyFunction); - } - result.push(newChunk); - result.push(chunk.prepareDestroyPermanently()); - break; - } - } - - if (result.length && !prepareRecordsOnly) { - await this.batchRecords(result); - } - - return result; - }; - - handleReceivedPostsInChannel = async (posts?: Post[], prepareRecordsOnly = false): Promise => { - if (!posts?.length) { - logWarning( - 'An empty or undefined "posts" array has been passed to the handleReceivedPostsInChannel method', - ); - return []; - } - - const {firstPost, lastPost} = getPostListEdges(posts); - - // Channel Id for this chain of posts - const channelId = firstPost.channel_id; - - // Find smallest 'create_at' value in chain - const earliest = firstPost.create_at; - - // Find highest 'create_at' value in chain; -1 means we are dealing with one item in the posts array - const latest = lastPost.create_at; - - // Find the records in the PostsInChannel table that have a matching channel_id - const chunks = (await this.database.get(POSTS_IN_CHANNEL).query( - Q.where('channel_id', channelId), - Q.sortBy('latest', Q.desc), - ).fetch()) as PostsInChannelModel[]; - - // chunk length 0; then it's a new chunk to be added to the PostsInChannel table - if (chunks.length === 0) { - return this._createPostsInChannelRecord(channelId, earliest, latest, prepareRecordsOnly); - } - - let targetChunk: PostsInChannelModel|undefined; - - for (const chunk of chunks) { - // find if we should plug the chain before - if (firstPost.create_at >= chunk.earliest || latest <= chunk.latest) { - targetChunk = chunk; - break; - } - } - - if (targetChunk) { - if ( - targetChunk.earliest <= earliest && - targetChunk.latest >= latest - ) { - return []; - } - - // If the chunk was found, Update the chunk and return - if (prepareRecordsOnly) { - targetChunk.prepareUpdate((record) => { - record.earliest = Math.min(record.earliest, earliest); - record.latest = Math.max(record.latest, latest); - }); - return [targetChunk]; - } - - targetChunk = await this.database.write(async () => { - return targetChunk!.update((record) => { - record.earliest = Math.min(record.earliest, earliest); - record.latest = Math.max(record.latest, latest); - }); - }); - - return [targetChunk!]; - } - - // Create a new chunk and merge them if needed - const newChunk = await this._createPostsInChannelRecord(channelId, earliest, latest, prepareRecordsOnly); - const merged = await this._mergePostInChannelChunks(newChunk[0], chunks, prepareRecordsOnly); - return merged; - }; - - handleReceivedPostsInChannelSince = async (posts: Post[], prepareRecordsOnly = false): Promise => { - if (!posts?.length) { - logWarning( - 'An empty or undefined "posts" array has been passed to the handleReceivedPostsInChannelSince method', - ); - return []; - } - - const {firstPost} = getPostListEdges(posts); - let latest = 0; - - let recentChunk: PostsInChannelModel|undefined; - const chunks = (await this.database.get(POSTS_IN_CHANNEL).query( - Q.where('channel_id', firstPost.channel_id), - Q.sortBy('latest', Q.desc), - ).fetch()) as PostsInChannelModel[]; - - if (chunks.length) { - recentChunk = chunks[0]; - - // add any new recent post while skipping the ones that were just updated - for (const post of posts) { - if (post.create_at > recentChunk.latest) { - latest = post.create_at; - } - } - } - - if (recentChunk && recentChunk.latest < latest) { - // We've got new posts that belong to this chunk - if (prepareRecordsOnly) { - recentChunk.prepareUpdate((record) => { - record.latest = Math.max(record.latest, latest); - }); - - return [recentChunk]; - } - - recentChunk = await this.database.write(async () => { - return recentChunk!.update((record) => { - record.latest = Math.max(record.latest, latest); - }); - }); - - return [recentChunk!]; - } - - return []; - }; - - handleReceivedPostsInChannelBefore = async (posts: Post[], prepareRecordsOnly = false): Promise => { - if (!posts?.length) { - logWarning( - 'An empty or undefined "posts" array has been passed to the handleReceivedPostsInChannelBefore method', - ); - return []; - } - - const {firstPost} = getPostListEdges(posts); - - // Channel Id for this chain of posts - const channelId = firstPost.channel_id; - - // Find smallest 'create_at' value in chain - const earliest = firstPost.create_at; - - // Find the records in the PostsInChannel table that have a matching channel_id - const chunks = (await this.database.get(POSTS_IN_CHANNEL).query( - Q.where('channel_id', channelId), - Q.sortBy('latest', Q.desc), - ).fetch()) as PostsInChannelModel[]; - - if (chunks.length === 0) { - // No chunks found, previous posts in this block not found - return []; - } - - let targetChunk = chunks[0]; - if (targetChunk) { - if (targetChunk.earliest <= earliest) { - return []; - } - - // If the chunk was found, Update the chunk and return - if (prepareRecordsOnly) { - targetChunk.prepareUpdate((record) => { - record.earliest = Math.min(record.earliest, earliest); - }); - return [targetChunk]; - } - - targetChunk = await this.database.write(async () => { - return targetChunk!.update((record) => { - record.earliest = Math.min(record.earliest, earliest); - }); - }); - - return [targetChunk!]; - } - - return targetChunk; - }; - - handleReceivedPostsInChannelAfter = async (posts: Post[], prepareRecordsOnly = false): Promise => { - throw new Error(`handleReceivedPostsInChannelAfter Not implemented yet. posts count${posts.length} prepareRecordsOnly=${prepareRecordsOnly}`); - }; - - handleReceivedPostForChannel = async (posts: Post[], prepareRecordsOnly = false): Promise => { - if (!posts?.length) { - logWarning( - 'An empty or undefined "posts" array has been passed to the handleReceivedPostForChannel method', - ); - return []; - } - - const {firstPost, lastPost} = getPostListEdges(posts); - - // Channel Id for this chain of posts - const channelId = firstPost.channel_id; - - // Find smallest 'create_at' value in chain - const earliest = firstPost.create_at; - - // Find highest 'create_at' value in chain; -1 means we are dealing with one item in the posts array - const latest = lastPost.create_at; - - // Find the records in the PostsInChannel table that have a matching channel_id - const chunks = (await this.database.get(POSTS_IN_CHANNEL).query( - Q.where('channel_id', channelId), - Q.sortBy('latest', Q.desc), - ).fetch()) as PostsInChannelModel[]; - - // chunk length 0; then it's a new chunk to be added to the PostsInChannel table - if (chunks.length === 0) { - return this._createPostsInChannelRecord(channelId, earliest, latest, prepareRecordsOnly); - } - - let targetChunk = chunks[0]; - if (targetChunk) { - if (targetChunk.latest >= latest) { - return []; - } - - // If the chunk was found, Update the chunk and return - if (prepareRecordsOnly) { - targetChunk.prepareUpdate((record) => { - record.latest = Math.max(record.latest, latest); - }); - return [targetChunk]; - } - - targetChunk = await this.database.write(async () => { - return targetChunk!.update((record) => { - record.latest = Math.max(record.latest, latest); - }); - }); - - return [targetChunk!]; - } - - return targetChunk; - }; -}; - -export default PostsInChannelHandler; diff --git a/app/database/operator/server_data_operator/handlers/posts_in_thread.ts b/app/database/operator/server_data_operator/handlers/posts_in_thread.ts deleted file mode 100644 index 31b200a3c..000000000 --- a/app/database/operator/server_data_operator/handlers/posts_in_thread.ts +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {Q} from '@nozbe/watermelondb'; - -import {Database} from '@constants'; -import {getPostListEdges} from '@database//operator/utils/post'; -import {transformPostInThreadRecord} from '@database/operator/server_data_operator/transformers/post'; -import {getRawRecordPairs, getValidRecordsForUpdate} from '@database/operator/utils/general'; -import {logWarning} from '@utils/log'; - -import type {RecordPair} from '@typings/database/database'; -import type PostsInThreadModel from '@typings/database/models/servers/posts_in_thread'; - -export interface PostsInThreadHandlerMix { - handleReceivedPostsInThread: (postsMap: Record, prepareRecordsOnly?: boolean) => Promise; -} - -const {POSTS_IN_THREAD} = Database.MM_TABLES.SERVER; - -const PostsInThreadHandler = (superclass: any) => class extends superclass { - handleReceivedPostsInThread = async (postsMap: Record, prepareRecordsOnly = false): Promise => { - if (!postsMap || !Object.keys(postsMap).length) { - logWarning( - 'An empty or undefined "postsMap" object has been passed to the handleReceivedPostsInThread method', - ); - return []; - } - - const update: RecordPair[] = []; - const create: PostsInThread[] = []; - const ids = Object.keys(postsMap); - for await (const rootId of ids) { - const {firstPost, lastPost} = getPostListEdges(postsMap[rootId]); - const chunks = (await this.database.get(POSTS_IN_THREAD).query( - Q.where('root_id', rootId), - Q.sortBy('latest', Q.desc), - ).fetch()) as PostsInThreadModel[]; - - if (chunks.length) { - const chunk = chunks[0]; - const newValue = { - root_id: rootId, - earliest: Math.min(chunk.earliest, firstPost.create_at), - latest: Math.max(chunk.latest, lastPost.create_at), - }; - update.push(getValidRecordsForUpdate({ - tableName: POSTS_IN_THREAD, - newValue, - existingRecord: chunk, - })); - } else { - // create chunk - create.push({ - root_id: rootId, - earliest: firstPost.create_at, - latest: lastPost.create_at, - }); - } - } - - const postInThreadRecords = (await this.prepareRecords({ - createRaws: getRawRecordPairs(create), - updateRaws: update, - transformer: transformPostInThreadRecord, - tableName: POSTS_IN_THREAD, - })) as PostsInThreadModel[]; - - if (postInThreadRecords?.length && !prepareRecordsOnly) { - await this.batchRecords(postInThreadRecords); - } - - return postInThreadRecords; - }; -}; - -export default PostsInThreadHandler; diff --git a/app/database/operator/server_data_operator/handlers/reaction.ts b/app/database/operator/server_data_operator/handlers/reaction.ts deleted file mode 100644 index a4f8aebd4..000000000 --- a/app/database/operator/server_data_operator/handlers/reaction.ts +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {MM_TABLES} from '@constants/database'; -import {transformReactionRecord} from '@database/operator/server_data_operator/transformers/reaction'; -import {sanitizeReactions} from '@database/operator/utils/reaction'; -import {logWarning} from '@utils/log'; - -import type {HandleReactionsArgs} from '@typings/database/database'; -import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji'; -import type ReactionModel from '@typings/database/models/servers/reaction'; - -const {REACTION} = MM_TABLES.SERVER; - -export interface ReactionHandlerMix { - handleReactions: ({postsReactions, prepareRecordsOnly}: HandleReactionsArgs) => Promise>; -} - -const ReactionHandler = (superclass: any) => class extends superclass { - /** - * handleReactions: Handler responsible for the Create/Update operations occurring on the Reaction table from the 'Server' schema - * @param {HandleReactionsArgs} handleReactions - * @param {ReactionsPerPost[]} handleReactions.postsReactions - * @param {boolean} handleReactions.prepareRecordsOnly - * @param {boolean} handleReactions.skipSync - * @returns {Promise>} - */ - handleReactions = async ({postsReactions, prepareRecordsOnly, skipSync}: HandleReactionsArgs): Promise => { - const batchRecords: ReactionModel[] = []; - - if (!postsReactions?.length) { - logWarning( - 'An empty or undefined "postsReactions" array has been passed to the handleReactions method', - ); - return []; - } - - for await (const postReactions of postsReactions) { - const {post_id, reactions} = postReactions; - const { - createReactions, - deleteReactions, - } = await sanitizeReactions({ - database: this.database, - post_id, - rawReactions: reactions, - skipSync, - }); - - if (createReactions?.length) { - // Prepares record for model Reactions - const reactionsRecords = (await this.prepareRecords({ - createRaws: createReactions, - transformer: transformReactionRecord, - tableName: REACTION, - })) as ReactionModel[]; - batchRecords.push(...reactionsRecords); - } - - if (deleteReactions?.length && !skipSync) { - deleteReactions.forEach((outCast) => outCast.prepareDestroyPermanently()); - batchRecords.push(...deleteReactions); - } - } - - if (prepareRecordsOnly) { - return batchRecords; - } - - if (batchRecords?.length) { - await this.batchRecords(batchRecords); - } - - return batchRecords; - }; -}; - -export default ReactionHandler; diff --git a/app/database/operator/server_data_operator/handlers/team.test.ts b/app/database/operator/server_data_operator/handlers/team.test.ts index ac01b7859..7f048cf7c 100644 --- a/app/database/operator/server_data_operator/handlers/team.test.ts +++ b/app/database/operator/server_data_operator/handlers/team.test.ts @@ -60,7 +60,7 @@ describe('*** Operator: Team Handlers tests ***', () => { tableName: 'Team', prepareRecordsOnly: false, transformer: transformTeamRecord, - }); + }, 'handleTeam'); }); it('=> HandleTeamMemberships: should write to the TEAM_MEMBERSHIP table', async () => { @@ -98,7 +98,7 @@ describe('*** Operator: Team Handlers tests ***', () => { prepareRecordsOnly: false, buildKeyRecordBy: buildTeamMembershipKey, transformer: transformTeamMembershipRecord, - }); + }, 'handleTeamMemberships'); }); it('=> HandleMyTeam: should write to the MY_TEAM table', async () => { @@ -124,7 +124,7 @@ describe('*** Operator: Team Handlers tests ***', () => { tableName: 'MyTeam', prepareRecordsOnly: false, transformer: transformMyTeamRecord, - }); + }, 'handleMyTeam'); }); it('=> HandleTeamChannelHistory: should write to the TEAM_CHANNEL_HISTORY table', async () => { @@ -150,7 +150,7 @@ describe('*** Operator: Team Handlers tests ***', () => { tableName: 'TeamChannelHistory', prepareRecordsOnly: false, transformer: transformTeamChannelHistoryRecord, - }); + }, 'handleTeamChannelHistory'); }); it('=> HandleTeamSearchHistory: should write to the TEAM_SEARCH_HISTORY table', async () => { @@ -179,6 +179,6 @@ describe('*** Operator: Team Handlers tests ***', () => { prepareRecordsOnly: false, buildKeyRecordBy: buildTeamSearchHistoryKey, transformer: transformTeamSearchHistoryRecord, - }); + }, 'handleTeamSearchHistory'); }); }); diff --git a/app/database/operator/server_data_operator/handlers/team.ts b/app/database/operator/server_data_operator/handlers/team.ts index 86d1d6070..42dc3f292 100644 --- a/app/database/operator/server_data_operator/handlers/team.ts +++ b/app/database/operator/server_data_operator/handlers/team.ts @@ -18,6 +18,7 @@ import { import {getUniqueRawsBy} from '@database/operator/utils/general'; import {logWarning} from '@utils/log'; +import type ServerDataOperatorBase from '.'; import type { HandleMyTeamArgs, HandleTeamArgs, HandleTeamChannelHistoryArgs, HandleTeamMembershipArgs, HandleTeamSearchHistoryArgs, @@ -44,7 +45,7 @@ export interface TeamHandlerMix { handleMyTeam: ({myTeams, prepareRecordsOnly}: HandleMyTeamArgs) => Promise; } -const TeamHandler = (superclass: any) => class extends superclass { +const TeamHandler = >(superclass: TBase) => class extends superclass { /** * handleTeamMemberships: Handler responsible for the Create/Update operations occurring on the TEAM_MEMBERSHIP table from the 'Server' schema * @param {HandleTeamMembershipArgs} teamMembershipsArgs @@ -97,7 +98,7 @@ const TeamHandler = (superclass: any) => class extends superclass { createOrUpdateRawValues, tableName: TEAM_MEMBERSHIP, prepareRecordsOnly, - }); + }, 'handleTeamMemberships'); }; /** @@ -146,7 +147,7 @@ const TeamHandler = (superclass: any) => class extends superclass { prepareRecordsOnly, createOrUpdateRawValues, tableName: TEAM, - }); + }, 'handleTeam'); }; /** @@ -172,7 +173,7 @@ const TeamHandler = (superclass: any) => class extends superclass { prepareRecordsOnly, createOrUpdateRawValues, tableName: TEAM_CHANNEL_HISTORY, - }); + }, 'handleTeamChannelHistory'); }; /** @@ -199,7 +200,7 @@ const TeamHandler = (superclass: any) => class extends superclass { prepareRecordsOnly, createOrUpdateRawValues, tableName: TEAM_SEARCH_HISTORY, - }); + }, 'handleTeamSearchHistory'); }; /** @@ -248,7 +249,7 @@ const TeamHandler = (superclass: any) => class extends superclass { prepareRecordsOnly, createOrUpdateRawValues, tableName: MY_TEAM, - }); + }, 'handleMyTeam'); }; }; diff --git a/app/database/operator/server_data_operator/handlers/team_threads_sync.ts b/app/database/operator/server_data_operator/handlers/team_threads_sync.ts index b4c096c2c..e53f9702d 100644 --- a/app/database/operator/server_data_operator/handlers/team_threads_sync.ts +++ b/app/database/operator/server_data_operator/handlers/team_threads_sync.ts @@ -8,6 +8,7 @@ import {transformTeamThreadsSyncRecord} from '@database/operator/server_data_ope import {getRawRecordPairs, getUniqueRawsBy, getValidRecordsForUpdate} from '@database/operator/utils/general'; import {logWarning} from '@utils/log'; +import type ServerDataOperatorBase from '.'; import type {HandleTeamThreadsSyncArgs, RecordPair} from '@typings/database/database'; import type TeamThreadsSyncModel from '@typings/database/models/servers/team_threads_sync'; @@ -17,7 +18,7 @@ export interface TeamThreadsSyncHandlerMix { const {TEAM_THREADS_SYNC} = MM_TABLES.SERVER; -const TeamThreadsSyncHandler = (superclass: any) => class extends superclass { +const TeamThreadsSyncHandler = >(superclass: TBase) => class extends superclass { handleTeamThreadsSync = async ({data, prepareRecordsOnly = false}: HandleTeamThreadsSyncArgs): Promise => { if (!data || !data.length) { logWarning( @@ -61,7 +62,7 @@ const TeamThreadsSyncHandler = (superclass: any) => class extends superclass { })) as TeamThreadsSyncModel[]; if (models?.length && !prepareRecordsOnly) { - await this.batchRecords(models); + await this.batchRecords(models, 'handleTeamThreadsSync'); } return models; diff --git a/app/database/operator/server_data_operator/handlers/thread.test.ts b/app/database/operator/server_data_operator/handlers/thread.test.ts index b7a987fb3..e7ce781ef 100644 --- a/app/database/operator/server_data_operator/handlers/thread.test.ts +++ b/app/database/operator/server_data_operator/handlers/thread.test.ts @@ -59,7 +59,7 @@ describe('*** Operator: Thread Handlers tests ***', () => { createOrUpdateRawValues: threads, tableName: 'Thread', prepareRecordsOnly: true, - }); + }, 'handleThreads(NEVER)'); // Should handle participants expect(spyOnHandleThreadParticipants).toHaveBeenCalledWith({ diff --git a/app/database/operator/server_data_operator/handlers/thread.ts b/app/database/operator/server_data_operator/handlers/thread.ts index 5cb71befe..735ae2b20 100644 --- a/app/database/operator/server_data_operator/handlers/thread.ts +++ b/app/database/operator/server_data_operator/handlers/thread.ts @@ -7,14 +7,16 @@ import {MM_TABLES} from '@constants/database'; import { transformThreadRecord, transformThreadParticipantRecord, + transformThreadInTeamRecord, } from '@database/operator/server_data_operator/transformers/thread'; -import {getUniqueRawsBy} from '@database/operator/utils/general'; +import {getRawRecordPairs, getUniqueRawsBy} from '@database/operator/utils/general'; import {sanitizeThreadParticipants} from '@database/operator/utils/thread'; import {logWarning} from '@utils/log'; +import type ServerDataOperatorBase from '.'; import type Database from '@nozbe/watermelondb/Database'; import type Model from '@nozbe/watermelondb/Model'; -import type {HandleThreadsArgs, HandleThreadParticipantsArgs} from '@typings/database/database'; +import type {HandleThreadsArgs, HandleThreadParticipantsArgs, HandleThreadInTeamArgs} from '@typings/database/database'; import type ThreadModel from '@typings/database/models/servers/thread'; import type ThreadInTeamModel from '@typings/database/models/servers/thread_in_team'; import type ThreadParticipantModel from '@typings/database/models/servers/thread_participant'; @@ -22,14 +24,16 @@ import type ThreadParticipantModel from '@typings/database/models/servers/thread const { THREAD, THREAD_PARTICIPANT, + THREADS_IN_TEAM, } = MM_TABLES.SERVER; export interface ThreadHandlerMix { handleThreads: ({threads, teamId, prepareRecordsOnly}: HandleThreadsArgs) => Promise; handleThreadParticipants: ({threadsParticipants, prepareRecordsOnly}: HandleThreadParticipantsArgs) => Promise; + handleThreadInTeam: ({threadsMap, prepareRecordsOnly}: HandleThreadInTeamArgs) => Promise; } -const ThreadHandler = (superclass: any) => class extends superclass { +const ThreadHandler = >(superclass: TBase) => class extends superclass { /** * handleThreads: Handler responsible for the Create/Update operations occurring on the Thread table from the 'Server' schema * @param {HandleThreadsArgs} handleThreads @@ -49,11 +53,11 @@ const ThreadHandler = (superclass: any) => class extends superclass { const uniqueThreads = getUniqueRawsBy({ raws: threads, key: 'id', - }) as Thread[]; + }) as ThreadWithLastFetchedAt[]; // Seperate threads to be deleted & created/updated const deletedThreadIds: string[] = []; - const createOrUpdateThreads: Thread[] = []; + const createOrUpdateThreads: ThreadWithLastFetchedAt[] = []; uniqueThreads.forEach((thread) => { if (thread.delete_at > 0) { deletedThreadIds.push(thread.id); @@ -106,7 +110,7 @@ const ThreadHandler = (superclass: any) => class extends superclass { prepareRecordsOnly: true, createOrUpdateRawValues: createOrUpdateThreads, tableName: THREAD, - }) as ThreadModel[]; + }, 'handleThreads(NEVER)'); // Add the models to be batched here const batch: Model[] = [...preparedThreads]; @@ -124,7 +128,7 @@ const ThreadHandler = (superclass: any) => class extends superclass { } if (batch.length && !prepareRecordsOnly) { - await this.batchRecords(batch); + await this.batchRecords(batch, 'handleThreads'); } return batch; @@ -175,11 +179,58 @@ const ThreadHandler = (superclass: any) => class extends superclass { } if (batchRecords?.length) { - await this.batchRecords(batchRecords); + await this.batchRecords(batchRecords, 'handleThreadParticipants'); } return batchRecords; }; + + handleThreadInTeam = async ({threadsMap, prepareRecordsOnly = false}: HandleThreadInTeamArgs): Promise => { + if (!threadsMap || !Object.keys(threadsMap).length) { + logWarning( + 'An empty or undefined "threadsMap" object has been passed to the handleReceivedPostForChannel method', + ); + return []; + } + + const create: ThreadInTeam[] = []; + const teamIds = Object.keys(threadsMap); + for await (const teamId of teamIds) { + const threadIds = threadsMap[teamId].map((thread) => thread.id); + const chunks = await (this.database as Database).get(THREADS_IN_TEAM).query( + Q.where('team_id', teamId), + Q.where('id', Q.oneOf(threadIds)), + ).fetch(); + const chunksMap = chunks.reduce((result: Record, chunk) => { + result[chunk.threadId] = chunk; + return result; + }, {}); + + for (const thread of threadsMap[teamId]) { + const chunk = chunksMap[thread.id]; + + // Create if the chunk is not found + if (!chunk) { + create.push({ + thread_id: thread.id, + team_id: teamId, + }); + } + } + } + + const threadsInTeam = (await this.prepareRecords({ + createRaws: getRawRecordPairs(create), + transformer: transformThreadInTeamRecord, + tableName: THREADS_IN_TEAM, + })) as ThreadInTeamModel[]; + + if (threadsInTeam?.length && !prepareRecordsOnly) { + await this.batchRecords(threadsInTeam, 'handleThreadInTeam'); + } + + return threadsInTeam; + }; }; export default ThreadHandler; diff --git a/app/database/operator/server_data_operator/handlers/thread_in_team.ts b/app/database/operator/server_data_operator/handlers/thread_in_team.ts deleted file mode 100644 index f8076d16b..000000000 --- a/app/database/operator/server_data_operator/handlers/thread_in_team.ts +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {Q, Database} from '@nozbe/watermelondb'; - -import {MM_TABLES} from '@constants/database'; -import {transformThreadInTeamRecord} from '@database/operator/server_data_operator/transformers/thread'; -import {getRawRecordPairs} from '@database/operator/utils/general'; -import {logWarning} from '@utils/log'; - -import type {HandleThreadInTeamArgs} from '@typings/database/database'; -import type ThreadInTeamModel from '@typings/database/models/servers/thread_in_team'; - -export interface ThreadInTeamHandlerMix { - handleThreadInTeam: ({threadsMap, prepareRecordsOnly}: HandleThreadInTeamArgs) => Promise; -} - -const {THREADS_IN_TEAM} = MM_TABLES.SERVER; - -const ThreadInTeamHandler = (superclass: any) => class extends superclass { - handleThreadInTeam = async ({threadsMap, prepareRecordsOnly = false}: HandleThreadInTeamArgs): Promise => { - if (!threadsMap || !Object.keys(threadsMap).length) { - logWarning( - 'An empty or undefined "threadsMap" object has been passed to the handleReceivedPostForChannel method', - ); - return []; - } - - const create: ThreadInTeam[] = []; - const teamIds = Object.keys(threadsMap); - for await (const teamId of teamIds) { - const threadIds = threadsMap[teamId].map((thread) => thread.id); - const chunks = await (this.database as Database).get(THREADS_IN_TEAM).query( - Q.where('team_id', teamId), - Q.where('id', Q.oneOf(threadIds)), - ).fetch(); - const chunksMap = chunks.reduce((result: Record, chunk) => { - result[chunk.threadId] = chunk; - return result; - }, {}); - - for (const thread of threadsMap[teamId]) { - const chunk = chunksMap[thread.id]; - - // Create if the chunk is not found - if (!chunk) { - create.push({ - thread_id: thread.id, - team_id: teamId, - }); - } - } - } - - const threadsInTeam = (await this.prepareRecords({ - createRaws: getRawRecordPairs(create), - transformer: transformThreadInTeamRecord, - tableName: THREADS_IN_TEAM, - })) as ThreadInTeamModel[]; - - if (threadsInTeam?.length && !prepareRecordsOnly) { - await this.batchRecords(threadsInTeam); - } - - return threadsInTeam; - }; -}; - -export default ThreadInTeamHandler; diff --git a/app/database/operator/server_data_operator/handlers/user.test.ts b/app/database/operator/server_data_operator/handlers/user.test.ts index 360930e85..7f47c4702 100644 --- a/app/database/operator/server_data_operator/handlers/user.test.ts +++ b/app/database/operator/server_data_operator/handlers/user.test.ts @@ -102,7 +102,7 @@ describe('*** Operator: User Handlers tests ***', () => { tableName: 'User', prepareRecordsOnly: false, transformer: transformUserRecord, - }); + }, 'handleUsers'); }); it('=> HandlePreferences: should write to the PREFERENCE table', async () => { @@ -151,6 +151,6 @@ describe('*** Operator: User Handlers tests ***', () => { prepareRecordsOnly: true, buildKeyRecordBy: buildPreferenceKey, transformer: transformPreferenceRecord, - }); + }, 'handlePreferences(NEVER)'); }); }); diff --git a/app/database/operator/server_data_operator/handlers/user.ts b/app/database/operator/server_data_operator/handlers/user.ts index bf62b8285..b375b779e 100644 --- a/app/database/operator/server_data_operator/handlers/user.ts +++ b/app/database/operator/server_data_operator/handlers/user.ts @@ -11,6 +11,7 @@ import {getUniqueRawsBy} from '@database/operator/utils/general'; import {filterPreferences} from '@helpers/api/preference'; import {logWarning} from '@utils/log'; +import type ServerDataOperatorBase from '.'; import type { HandlePreferencesArgs, HandleUsersArgs, @@ -25,7 +26,7 @@ export interface UserHandlerMix { handleUsers: ({users, prepareRecordsOnly}: HandleUsersArgs) => Promise; } -const UserHandler = (superclass: any) => class extends superclass { +const UserHandler = >(superclass: TBase) => class extends superclass { /** * handlePreferences: Handler responsible for the Create/Update operations occurring on the PREFERENCE table from the 'Server' schema * @param {HandlePreferencesArgs} preferencesArgs @@ -87,7 +88,7 @@ const UserHandler = (superclass: any) => class extends superclass { prepareRecordsOnly: true, createOrUpdateRawValues, tableName: PREFERENCE, - }); + }, 'handlePreferences(NEVER)'); records.push(...createOrUpdate); } @@ -96,7 +97,7 @@ const UserHandler = (superclass: any) => class extends superclass { } if (records.length && !prepareRecordsOnly) { - await this.batchRecords(records); + await this.batchRecords(records, 'handlePreferences'); } return records; @@ -125,7 +126,7 @@ const UserHandler = (superclass: any) => class extends superclass { createOrUpdateRawValues, tableName: USER, prepareRecordsOnly, - }); + }, 'handleUsers'); }; }; diff --git a/app/database/operator/server_data_operator/index.ts b/app/database/operator/server_data_operator/index.ts index 74a8c5b39..db1560103 100644 --- a/app/database/operator/server_data_operator/index.ts +++ b/app/database/operator/server_data_operator/index.ts @@ -6,13 +6,9 @@ import CategoryHandler, {CategoryHandlerMix} from '@database/operator/server_dat import ChannelHandler, {ChannelHandlerMix} from '@database/operator/server_data_operator/handlers/channel'; import GroupHandler, {GroupHandlerMix} from '@database/operator/server_data_operator/handlers/group'; import PostHandler, {PostHandlerMix} from '@database/operator/server_data_operator/handlers/post'; -import PostsInChannelHandler, {PostsInChannelHandlerMix} from '@database/operator/server_data_operator/handlers/posts_in_channel'; -import PostsInThreadHandler, {PostsInThreadHandlerMix} from '@database/operator/server_data_operator/handlers/posts_in_thread'; -import ReactionHander, {ReactionHandlerMix} from '@database/operator/server_data_operator/handlers/reaction'; import TeamHandler, {TeamHandlerMix} from '@database/operator/server_data_operator/handlers/team'; import TeamThreadsSyncHandler, {TeamThreadsSyncHandlerMix} from '@database/operator/server_data_operator/handlers/team_threads_sync'; import ThreadHandler, {ThreadHandlerMix} from '@database/operator/server_data_operator/handlers/thread'; -import ThreadInTeamHandler, {ThreadInTeamHandlerMix} from '@database/operator/server_data_operator/handlers/thread_in_team'; import UserHandler, {UserHandlerMix} from '@database/operator/server_data_operator/handlers/user'; import mix from '@utils/mix'; @@ -23,13 +19,9 @@ interface ServerDataOperator extends ChannelHandlerMix, GroupHandlerMix, PostHandlerMix, - PostsInChannelHandlerMix, - PostsInThreadHandlerMix, - ReactionHandlerMix, ServerDataOperatorBase, TeamHandlerMix, ThreadHandlerMix, - ThreadInTeamHandlerMix, TeamThreadsSyncHandlerMix, UserHandlerMix {} @@ -39,12 +31,8 @@ class ServerDataOperator extends mix(ServerDataOperatorBase).with( ChannelHandler, GroupHandler, PostHandler, - PostsInChannelHandler, - PostsInThreadHandler, - ReactionHander, TeamHandler, ThreadHandler, - ThreadInTeamHandler, TeamThreadsSyncHandler, UserHandler, ) { diff --git a/app/database/operator/utils/general.ts b/app/database/operator/utils/general.ts index d858570cb..5d33bd4d2 100644 --- a/app/database/operator/utils/general.ts +++ b/app/database/operator/utils/general.ts @@ -3,6 +3,7 @@ import {MM_TABLES} from '@constants/database'; +import type {Model} from '@nozbe/watermelondb'; import type {IdenticalRecordArgs, RangeOfValueArgs, RecordPair, RetrieveRecordsArgs} from '@typings/database/database'; import type ChannelModel from '@typings/database/models/servers/channel'; import type PostModel from '@typings/database/models/servers/post'; @@ -92,6 +93,6 @@ export const getUniqueRawsBy = ({raws, key}: { raws: RawValue[]; key: string}) = * @param {Clause} records.condition * @returns {Promise} */ -export const retrieveRecords = ({database, tableName, condition}: RetrieveRecordsArgs) => { - return database.collections.get(tableName).query(condition).fetch(); +export const retrieveRecords = ({database, tableName, condition}: RetrieveRecordsArgs) => { + return database.collections.get(tableName).query(condition).fetch(); }; diff --git a/app/managers/draft_upload_manager/index.test.ts b/app/managers/draft_upload_manager/index.test.ts index 545dcf60b..044174b5d 100644 --- a/app/managers/draft_upload_manager/index.test.ts +++ b/app/managers/draft_upload_manager/index.test.ts @@ -84,13 +84,13 @@ describe('draft upload manager', () => { const fileClientId = 'clientId'; const fileServerId = 'serverId'; - await addFilesToDraft(url, channelId, rootId, [{clientId: fileClientId} as FileInfo]); + await addFilesToDraft(url, channelId, rootId, [{clientId: fileClientId, localPath: 'path1'} as FileInfo]); - manager.prepareUpload(url, {clientId: fileClientId} as FileInfo, channelId, rootId, 0); + manager.prepareUpload(url, {clientId: fileClientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0); expect(manager.isUploading(fileClientId)).toBe(true); expect(uploadMocks.resolvePromise).not.toBeNull(); - uploadMocks.resolvePromise!({ok: true, code: 201, data: {file_infos: [{clientId: fileClientId, id: fileServerId}]}}); + uploadMocks.resolvePromise!({ok: true, code: 201, data: {file_infos: [{clientId: fileClientId, id: fileServerId, localPath: 'path1'}]}}); // Wait for other promises (on complete write) to finish await new Promise(process.nextTick); @@ -107,9 +107,9 @@ describe('draft upload manager', () => { const uploadMocks = mockUpload(); const fileClientId = 'clientId'; - await addFilesToDraft(url, channelId, rootId, [{clientId: fileClientId} as FileInfo]); + await addFilesToDraft(url, channelId, rootId, [{clientId: fileClientId, localPath: 'path1'} as FileInfo]); - manager.prepareUpload(url, {clientId: fileClientId} as FileInfo, channelId, rootId, 0); + manager.prepareUpload(url, {clientId: fileClientId, localPath: 'path2'} as FileInfo, channelId, rootId, 0); expect(manager.isUploading(fileClientId)).toBe(true); // Wait for other promises to finish @@ -260,13 +260,13 @@ describe('draft upload manager', () => { const fileClientId = 'clientId'; const fileServerId = 'serverId'; - await addFilesToDraft(url, channelId, rootId, [{clientId: fileClientId} as FileInfo]); + await addFilesToDraft(url, channelId, rootId, [{clientId: fileClientId, localPath: 'path1'} as FileInfo]); - manager.prepareUpload(url, {clientId: fileClientId} as FileInfo, channelId, rootId, 0); + manager.prepareUpload(url, {clientId: fileClientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0); expect(manager.isUploading(fileClientId)).toBe(true); expect(uploadMocks.resolvePromise).not.toBeNull(); - uploadMocks.resolvePromise!({ok: true, code: 500, data: {file_infos: [{clientId: fileClientId, id: fileServerId}]}}); + uploadMocks.resolvePromise!({ok: true, code: 500, data: {file_infos: [{clientId: fileClientId, id: fileServerId, localPath: 'path1'}]}}); // Wait for other promises (on complete write) to finish await new Promise(process.nextTick); @@ -284,9 +284,9 @@ describe('draft upload manager', () => { const uploadMocks = mockUpload(); const clientId = 'clientId'; - await addFilesToDraft(url, channelId, rootId, [{clientId} as FileInfo]); + await addFilesToDraft(url, channelId, rootId, [{clientId, localPath: 'path1'} as FileInfo]); - manager.prepareUpload(url, {clientId} as FileInfo, channelId, rootId, 0); + manager.prepareUpload(url, {clientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0); expect(manager.isUploading(clientId)).toBe(true); expect(uploadMocks.resolvePromise).not.toBeNull(); @@ -308,9 +308,9 @@ describe('draft upload manager', () => { const uploadMocks = mockUpload(); const clientId = 'clientId'; - await addFilesToDraft(url, channelId, rootId, [{clientId} as FileInfo]); + await addFilesToDraft(url, channelId, rootId, [{clientId, localPath: 'path1'} as FileInfo]); - manager.prepareUpload(url, {clientId} as FileInfo, channelId, rootId, 0); + manager.prepareUpload(url, {clientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0); expect(manager.isUploading(clientId)).toBe(true); expect(uploadMocks.resolvePromise).not.toBeNull(); @@ -332,13 +332,13 @@ describe('draft upload manager', () => { const uploadMocks = mockUpload(); const clientId = 'clientId'; - await addFilesToDraft(url, channelId, rootId, [{clientId} as FileInfo]); + await addFilesToDraft(url, channelId, rootId, [{clientId, localPath: 'path1'} as FileInfo]); const nullProgressHandler = jest.fn(); let cancelProgressHandler = manager.registerProgressHandler(clientId, nullProgressHandler); expect(cancelProgressHandler).toBeNull(); - manager.prepareUpload(url, {clientId} as FileInfo, channelId, rootId, 0); + manager.prepareUpload(url, {clientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0); expect(manager.isUploading(clientId)).toBe(true); const progressHandler = jest.fn(); @@ -373,13 +373,13 @@ describe('draft upload manager', () => { const uploadMocks = mockUpload(); const clientId = 'clientId'; - await addFilesToDraft(url, channelId, rootId, [{clientId} as FileInfo]); + await addFilesToDraft(url, channelId, rootId, [{clientId, localPath: 'path1'} as FileInfo]); const nullErrorHandler = jest.fn(); let cancelErrorHandler = manager.registerProgressHandler(clientId, nullErrorHandler); expect(cancelErrorHandler).toBeNull(); - manager.prepareUpload(url, {clientId} as FileInfo, channelId, rootId, 0); + manager.prepareUpload(url, {clientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0); expect(manager.isUploading(clientId)).toBe(true); const errorHandler = jest.fn(); @@ -409,13 +409,13 @@ describe('draft upload manager', () => { const uploadMocks = mockUpload(); const clientId = 'clientId'; - await addFilesToDraft(url, channelId, rootId, [{clientId} as FileInfo]); + await addFilesToDraft(url, channelId, rootId, [{clientId, localPath: 'path1'} as FileInfo]); const nullErrorHandler = jest.fn(); let cancelErrorHandler = manager.registerProgressHandler(clientId, nullErrorHandler); expect(cancelErrorHandler).toBeNull(); - manager.prepareUpload(url, {clientId} as FileInfo, channelId, rootId, 0); + manager.prepareUpload(url, {clientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0); expect(manager.isUploading(clientId)).toBe(true); const errorHandler = jest.fn(); diff --git a/app/queries/servers/channel.ts b/app/queries/servers/channel.ts index 28453a1e4..7ac1136e8 100644 --- a/app/queries/servers/channel.ts +++ b/app/queries/servers/channel.ts @@ -319,7 +319,7 @@ export async function deleteChannelMembership(operator: ServerDataOperator, user models.push(membership.prepareDestroyPermanently()); } if (models.length && !prepareRecordsOnly) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'deleteChannelMembership'); } return {models}; } catch (error) { diff --git a/app/queries/servers/preference.ts b/app/queries/servers/preference.ts index d86ee6e9a..95bb9a255 100644 --- a/app/queries/servers/preference.ts +++ b/app/queries/servers/preference.ts @@ -60,7 +60,7 @@ export async function deletePreferences(database: ServerDatabase, preferences: P } } if (preparedModels.length) { - await database.operator.batchRecords(preparedModels); + await database.operator.batchRecords(preparedModels, 'deletePreferences'); } return true; } catch (error) { diff --git a/app/queries/servers/system.ts b/app/queries/servers/system.ts index 194fe48b2..e17bc40cc 100644 --- a/app/queries/servers/system.ts +++ b/app/queries/servers/system.ts @@ -420,7 +420,7 @@ export async function setCurrentChannelId(operator: ServerDataOperator, channelI try { const models = await prepareCommonSystemValues(operator, {currentChannelId: channelId}); if (models) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'setCurrentChannelId'); } return {currentChannelId: channelId}; @@ -436,7 +436,7 @@ export async function setCurrentTeamAndChannelId(operator: ServerDataOperator, t currentTeamId: teamId, }); if (models) { - await operator.batchRecords(models); + await operator.batchRecords(models, 'setCurrentTeamAndChannelId'); } return {currentTeamId: teamId, currentChannelId: channelId}; diff --git a/app/queries/servers/team.ts b/app/queries/servers/team.ts index c328da4d9..2d71b20e6 100644 --- a/app/queries/servers/team.ts +++ b/app/queries/servers/team.ts @@ -214,7 +214,7 @@ export async function deleteMyTeams(operator: ServerDataOperator, myTeams: MyTea } if (preparedModels.length) { - await operator.batchRecords(preparedModels); + await operator.batchRecords(preparedModels, 'deleteMyTeams'); } return {}; } catch (error) { diff --git a/app/utils/mix.ts b/app/utils/mix.ts index 4d89a7f1b..fb1d709c4 100644 --- a/app/utils/mix.ts +++ b/app/utils/mix.ts @@ -1,17 +1,17 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -class MixinBuilder { - superclass: any; - constructor(superclass: any) { +class MixinBuilder { + superclass: T; + constructor(superclass: T) { this.superclass = superclass; } - with(...mixins: any[]) { + with(...mixins: any[]): T { return mixins.reduce((c, mixin) => mixin(c), this.superclass); } } -const mix = (superclass: any) => new MixinBuilder(superclass); +const mix = (superclass: T) => new MixinBuilder(superclass); export default mix; diff --git a/patches/@nozbe+watermelondb+0.25.5.patch b/patches/@nozbe+watermelondb+0.25.5.patch index bc220b5cb..da655ee0f 100644 --- a/patches/@nozbe+watermelondb+0.25.5.patch +++ b/patches/@nozbe+watermelondb+0.25.5.patch @@ -1,8 +1,19 @@ diff --git a/node_modules/@nozbe/watermelondb/Database/index.js b/node_modules/@nozbe/watermelondb/Database/index.js -index 8d71c6f..9ad75d6 100644 +index 8d71c6f..30832c8 100644 --- a/node_modules/@nozbe/watermelondb/Database/index.js +++ b/node_modules/@nozbe/watermelondb/Database/index.js -@@ -126,6 +126,10 @@ var Database = /*#__PURE__*/function () { +@@ -91,7 +91,9 @@ var Database = /*#__PURE__*/function () { + + if (!preparedState) { + (0, _common.invariant)('disposable' !== record._raw._status, "Cannot batch a disposable record"); +- throw new Error("Cannot batch a record that doesn't have a prepared create/update/delete"); ++ //throw new Error("Cannot batch a record that doesn't have a prepared create/update/delete"); ++ console.debug('Trying to batch a record with no prepared state on table', record.constructor.table) ++ return; + } + + var raw = record._raw; +@@ -126,6 +128,10 @@ var Database = /*#__PURE__*/function () { // subsequent changes to the record don't trip up the invariant // TODO: What if this fails? record._preparedState = null; @@ -27,27 +38,37 @@ index 96114ec..ecfe3c1 100644 prepareDestroyPermanently(): this diff --git a/node_modules/@nozbe/watermelondb/Model/index.js b/node_modules/@nozbe/watermelondb/Model/index.js -index b0e3a83..57ecb8d 100644 +index b0e3a83..1bbce74 100644 --- a/node_modules/@nozbe/watermelondb/Model/index.js +++ b/node_modules/@nozbe/watermelondb/Model/index.js -@@ -81,7 +81,7 @@ var Model = /*#__PURE__*/function () { +@@ -81,7 +81,17 @@ var Model = /*#__PURE__*/function () { _proto.prepareUpdate = function prepareUpdate(recordUpdater = _noop.default) { var _this = this; - (0, _invariant.default)(!this._preparedState, "Cannot update a record with pending changes"); -+ (0, _invariant.default)(!this._preparedState, "Cannot update a record with pending changes in table " + _this.table); ++ if ('deleted' === this._raw._status) { ++ console.debug("Updating a deleted record in table " + _this.table) ++ return this; ++ } ++ ++ //(0, _invariant.default)(!this._preparedState, "Cannot update a record with pending changes"); ++ if (this._preparedState) { ++ console.debug("Updating a record with pending changes in table " + _this.table) ++ } else { ++ this.__original = Object.assign({}, this._raw); ++ } this.__ensureNotDisposable("Model.prepareUpdate()"); -@@ -92,6 +92,7 @@ var Model = /*#__PURE__*/function () { +@@ -91,7 +101,6 @@ var Model = /*#__PURE__*/function () { + this._setRaw((0, _Schema.columnName)('updated_at'), Date.now()); } // Perform updates - -+ this.__original = Object.assign({}, this._raw); +- (0, _ensureSync.default)(recordUpdater(this)); this._isEditing = false; this._preparedState = 'update'; // TODO: `process.nextTick` doesn't work on React Native -@@ -107,6 +108,19 @@ var Model = /*#__PURE__*/function () { +@@ -107,6 +116,19 @@ var Model = /*#__PURE__*/function () { return this; }; @@ -67,6 +88,18 @@ index b0e3a83..57ecb8d 100644 _proto.prepareMarkAsDeleted = function prepareMarkAsDeleted() { (0, _invariant.default)(!this._preparedState, "Cannot mark a record with pending changes as deleted"); +@@ -118,7 +140,10 @@ var Model = /*#__PURE__*/function () { + }; + + _proto.prepareDestroyPermanently = function prepareDestroyPermanently() { +- (0, _invariant.default)(!this._preparedState, "Cannot destroy permanently a record with pending changes"); ++ //(0, _invariant.default)(!this._preparedState, "Cannot destroy permanently a record with pending changes"); ++ if (this._preparedState) { ++ console.debug("Deleting a record with pending changes in table " + this.table); ++ } + + this.__ensureNotDisposable("Model.prepareDestroyPermanently()"); + diff --git a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/Database.kt b/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/Database.kt index ca31e20..b45c753 100644 --- a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/Database.kt diff --git a/test/test_helper.ts b/test/test_helper.ts index f31928fb4..4d5b9a0b6 100644 --- a/test/test_helper.ts +++ b/test/test_helper.ts @@ -104,7 +104,7 @@ class TestHelper { currentUserId: this.basicUser!.id, }); if (systems?.length) { - await operator.batchRecords(systems); + await operator.batchRecords(systems, 'test'); } await operator.handleSystem({ @@ -459,23 +459,23 @@ class TestHelper { }; mockLogin = () => { - nock(this.basicClient?.getBaseRoute()). + nock(this.basicClient?.getBaseRoute() || ''). post('/users/login'). reply(200, this.basicUser!, {'X-Version-Id': 'Server Version'}); - nock(this.basicClient?.getBaseRoute()). + nock(this.basicClient?.getBaseRoute() || ''). get('/users/me/teams/members'). reply(200, [this.basicTeamMember]); - nock(this.basicClient?.getBaseRoute()). + nock(this.basicClient?.getBaseRoute() || ''). get('/users/me/teams/unread'). reply(200, [{team_id: this.basicTeam!.id, msg_count: 0, mention_count: 0}]); - nock(this.basicClient?.getBaseRoute()). + nock(this.basicClient?.getBaseRoute() || ''). get('/users/me/teams'). reply(200, [this.basicTeam]); - nock(this.basicClient?.getBaseRoute()). + nock(this.basicClient?.getBaseRoute() || ''). get('/users/me/preferences'). reply(200, [{user_id: this.basicUser!.id, category: 'tutorial_step', name: this.basicUser!.id, value: '999'}]); }; @@ -563,7 +563,7 @@ class TestHelper { }; initBasic = async (client = this.createClient()) => { - client.setUrl(Config.TestServerUrl || Config.DefaultServerUrl); + client.apiClient.baseUrl = Config.TestServerUrl || Config.DefaultServerUrl; this.basicClient = client; this.initMockEntities(); diff --git a/types/database/database.ts b/types/database/database.ts index b46dd201d..257dda66d 100644 --- a/types/database/database.ts +++ b/types/database/database.ts @@ -55,12 +55,12 @@ export type PrepareBaseRecordArgs = TransformerArgs & { fieldsMapper: (model: Model) => void; } -export type OperationArgs = { +export type OperationArgs = { tableName: string; createRaws?: RecordPair[]; updateRaws?: RecordPair[]; - deleteRaws?: Model[]; - transformer: (args: TransformerArgs) => Promise; + deleteRaws?: T[]; + transformer: (args: TransformerArgs) => Promise; }; export type Models = Array>; @@ -157,10 +157,10 @@ export type ProcessRecordsArgs = { buildKeyRecordBy?: (obj: Record) => string; }; -export type HandleRecordsArgs = { +export type HandleRecordsArgs = { buildKeyRecordBy?: (obj: Record) => string; fieldName: string; - transformer: (args: TransformerArgs) => Promise; + transformer: (args: TransformerArgs) => Promise; createOrUpdateRawValues: RawValue[]; deleteRawValues?: RawValue[]; tableName: string; @@ -311,8 +311,8 @@ export type GetDatabaseConnectionArgs = { setAsActiveDatabase: boolean; } -export type ProcessRecordResults = { +export type ProcessRecordResults = { createRaws: RecordPair[]; updateRaws: RecordPair[]; - deleteRaws: Model[]; + deleteRaws: T[]; } diff --git a/types/utils/mixins.d.ts b/types/utils/mixins.d.ts new file mode 100644 index 000000000..7da3788cb --- /dev/null +++ b/types/utils/mixins.d.ts @@ -0,0 +1,3 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +type Constructor = new (...args: any[]) => T;