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 <nahumhbl@gmail.com>
This commit is contained in:
Daniel Espino García 2023-02-03 16:11:57 +01:00 committed by GitHub
parent 449c5edac9
commit 980c31f40f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
83 changed files with 893 additions and 881 deletions

View file

@ -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};

View file

@ -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};

View file

@ -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};

View file

@ -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};

View file

@ -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);

View file

@ -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<Promise<Model[]>> = [];
@ -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) {

View file

@ -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) {

View file

@ -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<Chan
models.push(channel);
}
if (models?.length) {
await operator.batchRecords(models.flat());
await operator.batchRecords(models.flat(), 'patchChannel');
}
return {channel: channelData};
} catch (error) {
@ -343,7 +343,7 @@ export async function leaveChannel(serverUrl: string, channelId: string) {
models.push(...removeUserModels);
}
await operator.batchRecords(models);
await operator.batchRecords(models, 'leaveChannel');
if (isTabletDevice) {
switchToLastChannel(serverUrl);
@ -393,7 +393,7 @@ export async function fetchChannelCreator(serverUrl: string, channelId: string,
}));
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat());
await operator.batchRecords(models.flat(), 'fetchChannelCreator');
}
return {user};
@ -484,7 +484,7 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string,
const {models: catModels} = await storeCategories(serverUrl, categories, true, true); // Re-sync
const models = (chModels || []).concat(catModels || []);
if (models.length) {
await operator.batchRecords(models);
await operator.batchRecords(models, 'fetchMyChannelsForTeam');
}
setTeamLoading(serverUrl, false);
}
@ -609,7 +609,7 @@ export async function fetchMissingDirectChannelsInfo(serverUrl: string, directCh
}
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat());
await operator.batchRecords(models.flat(), 'fetchMissingDirectChannelInfo');
}
return {directChannels: updatedChannelsArray, users};
@ -687,7 +687,7 @@ export async function joinChannel(serverUrl: string, teamId: string, channelId?:
}
if (flattenedModels?.length > 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) {

View file

@ -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);

View file

@ -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');
}
}

View file

@ -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) {

View file

@ -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;

View file

@ -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))!;

View file

@ -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) => {

View file

@ -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};

View file

@ -128,7 +128,7 @@ export async function createPost(serverUrl: string, post: Partial<Post>, 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<Post>, 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<Post>, 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<PostModel>(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<PostModel>(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,

View file

@ -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};
}

View file

@ -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<Channel>(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<Channel>(directChannels);

View file

@ -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,

View file

@ -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);

View file

@ -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) {

View file

@ -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;

View file

@ -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};

View file

@ -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);

View file

@ -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);
}

View file

@ -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);

View file

@ -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

View file

@ -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<void> {
@ -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
}

View file

@ -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');
};

View file

@ -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
}

View file

@ -3,13 +3,15 @@
import {buildQueryString} from '@utils/helpers';
import type ClientBase from './base';
export interface ClientAppsMix {
executeAppCall: <Res = unknown>(call: AppCallRequest, trackAsSubmit: boolean) => Promise<AppCallResponse<Res>>;
getAppsBindings: (userID: string, channelID: string, teamID: string) => Promise<AppBinding[]>;
}
const ClientApps = (superclass: any) => class extends superclass {
executeAppCall = async <Res = unknown>(call: AppCallRequest, trackAsSubmit: boolean): Promise<AppCallResponse<Res>> => {
const ClientApps = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
executeAppCall = async (call: AppCallRequest, trackAsSubmit: boolean) => {
const callCopy = {
...call,
context: {

View file

@ -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, {

View file

@ -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<CategoriesWithOrder>;
getCategoriesOrder: (userId: string, teamId: string) => Promise<string[]>;
@ -8,7 +10,7 @@ export interface ClientCategoriesMix {
updateChannelCategories: (userId: string, teamId: string, categories: CategoryWithChannels[]) => Promise<CategoriesWithOrder>;
}
const ClientCategories = (superclass: any) => class extends superclass {
const ClientCategories = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getCategories = async (userId: string, teamId: string) => {
return this.doFetch(
`${this.getCategoriesRoute(userId, teamId)}`,

View file

@ -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<any>;
createChannel: (channel: Channel) => Promise<Channel>;
@ -44,7 +46,7 @@ export interface ClientChannelsMix {
getMemberInChannel: (channelId: string, userId: string) => Promise<any>;
}
const ClientChannels = (superclass: any) => class extends superclass {
const ClientChannels = <TBase extends Constructor<ClientBase>>(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<Channel>) => {
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)}`,

View file

@ -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<CustomEmoji>;
getCustomEmojiByName: (name: string) => Promise<CustomEmoji>;
@ -15,7 +17,7 @@ export interface ClientEmojisMix {
autocompleteCustomEmoji: (name: string) => Promise<CustomEmoji[]>;
}
const ClientEmojis = (superclass: any) => class extends superclass {
const ClientEmojis = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getCustomEmoji = async (id: string) => {
return this.doFetch(
`${this.getEmojisRoute()}/${id}`,

View file

@ -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<FileSearchRequest>;
}
const ClientFiles = (superclass: any) => class extends superclass {
const ClientFiles = <TBase extends Constructor<ClientBase>>(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<ClientResponse>;
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});
};

View file

@ -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<T> = {
policies: T[];
total_count: number;
@ -25,7 +27,7 @@ export interface ClientGeneralMix {
getRedirectLocation: (urlParam: string) => Promise<Record<string, string>>;
}
const ClientGeneral = (superclass: any) => class extends superclass {
const ClientGeneral = <TBase extends Constructor<ClientBase>>(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,
});

View file

@ -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<Group>;
getGroups: (params: {query?: string; filterAllowReference?: boolean; page?: number; perPage?: number; since?: number; includeMemberCount?: boolean}) => Promise<Group[]>;
@ -16,7 +18,7 @@ export interface ClientGroupsMix {
getAllTeamsAssociatedToGroup: (groupId: string, filterAllowReference?: boolean) => Promise<{groupTeams: GroupTeam[]}>;
}
const ClientGroups = (superclass: any) => class extends superclass {
const ClientGroups = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getGroup = async (id: string) => {
return this.doFetch(
`${this.urlVersion}/groups/${id}`,

View file

@ -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<Command[]>;
getCommandAutocompleteSuggestionsList: (userInput: string, teamId: string, channelId: string, rootId?: string) => Promise<AutocompleteSuggestion[]>;
@ -14,7 +16,7 @@ export interface ClientIntegrationsMix {
submitInteractiveDialog: (data: DialogSubmission) => Promise<any>;
}
const ClientIntegrations = (superclass: any) => class extends superclass {
const ClientIntegrations = <TBase extends Constructor<ClientBase>>(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},

View file

@ -3,11 +3,13 @@
import {General} from '@constants';
import type ClientBase from './base';
export interface ClientNPSMix {
npsGiveFeedbackAction: () => Promise<Post>;
}
const ClientNPS = (superclass: any) => class extends superclass {
const ClientNPS = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
npsGiveFeedbackAction = async () => {
return this.doFetch(
`${this.getPluginRoute(General.NPS_PLUGIN_ID)}/api/v1/give_feedback`,

View file

@ -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<ClientPluginManifest[]>;
}
const ClientPlugins = (superclass: any) => class extends superclass {
const ClientPlugins = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getPluginsManifests = async () => {
return this.doFetch(
`${this.getPluginsRoute()}/webapp`,

View file

@ -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<Post>;
updatePost: (post: Post) => Promise<Post>;
@ -31,12 +33,12 @@ export interface ClientPostsMix {
doPostActionWithCookie: (postId: string, actionId: string, actionCookie: string, selectedOption?: string) => Promise<any>;
}
const ClientPosts = (superclass: any) => class extends superclass {
const ClientPosts = <TBase extends Constructor<ClientBase>>(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<Post> & {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<PostResponse> => {
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 = {

View file

@ -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<any>;
deletePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;
getMyPreferences: () => Promise<PreferenceType[]>;
}
const ClientPreferences = (superclass: any) => class extends superclass {
const ClientPreferences = <TBase extends Constructor<ClientBase>>(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},

View file

@ -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<Team>;
deleteTeam: (teamId: string) => Promise<any>;
@ -28,9 +30,9 @@ export interface ClientTeamsMix {
getTeamIconUrl: (teamId: string, lastTeamIconUpdate: number) => string;
}
const ClientTeams = (superclass: any) => class extends superclass {
const ClientTeams = <TBase extends Constructor<ClientBase>>(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<Team> & {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)}`,

View file

@ -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<GetUserThreadsResponse>;
getThread: (userId: string, teamId: string, threadId: string, extended?: boolean) => Promise<any>;
@ -14,7 +16,7 @@ export interface ClientThreadsMix {
updateThreadFollow: (userId: string, teamId: string, threadId: string, state: boolean) => Promise<any>;
}
const ClientThreads = (superclass: any) => class extends superclass {
const ClientThreads = <TBase extends Constructor<ClientBase>>(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<string, any> = {
extended: 'true',

View file

@ -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<TermsOfService>;
}
const ClientTos = (superclass: any) => class extends superclass {
const ClientTos = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
updateMyTermsOfServiceStatus = async (termsOfServiceId: string, accepted: boolean) => {
return this.doFetch(
`${this.getUserRoute('me')}/terms_of_service`,

View file

@ -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<UserProfile>;
patchMe: (userPatch: Partial<UserProfile>) => Promise<UserProfile>;
@ -46,9 +48,9 @@ export interface ClientUsersMix {
removeRecentCustomStatus: (customStatus: UserCustomStatus) => Promise<{status: string}>;
}
const ClientUsers = (superclass: any) => class extends superclass {
const ClientUsers = <TBase extends Constructor<ClientBase>>(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<UserProfile> & {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`,

View file

@ -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');
});
});

View file

@ -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');
};
}

View file

@ -22,10 +22,10 @@ import type {
export interface BaseDataOperatorType {
database: Database;
handleRecords: ({buildKeyRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues, tableName, prepareRecordsOnly}: HandleRecordsArgs) => Promise<Model[]>;
processRecords: ({createOrUpdateRawValues, deleteRawValues, tableName, buildKeyRecordBy, fieldName}: ProcessRecordsArgs) => Promise<ProcessRecordResults>;
batchRecords: (models: Model[]) => Promise<void>;
prepareRecords: ({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs) => Promise<Model[]>;
handleRecords: <T extends Model>({buildKeyRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues, tableName, prepareRecordsOnly}: HandleRecordsArgs<T>, description: string) => Promise<Model[]>;
processRecords: <T extends Model>({createOrUpdateRawValues, deleteRawValues, tableName, buildKeyRecordBy, fieldName}: ProcessRecordsArgs) => Promise<ProcessRecordResults<T>>;
batchRecords: (models: Model[], description: string) => Promise<void>;
prepareRecords: <T extends Model>({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs<T>) => Promise<Model[]>;
}
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<ProcessRecordResults> => {
processRecords = async <T extends Model>({createOrUpdateRawValues = [], deleteRawValues = [], tableName, buildKeyRecordBy, fieldName}: ProcessRecordsArgs): Promise<ProcessRecordResults<T>> => {
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<T>({
database: this.database,
tableName,
condition: Q.where(fieldName, Q.oneOf(columnValues)),
@ -125,13 +125,13 @@ export default class BaseDataOperator {
* @param {(TransformerArgs) => Promise<Model>;} transformer
* @returns {Promise<Model[]>}
*/
prepareRecords = async ({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs): Promise<Model[]> => {
prepareRecords = async <T extends Model>({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs<T>): Promise<T[]> => {
if (!this.database) {
logWarning('Database not defined in prepareRecords');
return [];
}
let preparedRecords: Array<Promise<Model>> = [];
let preparedRecords: Array<Promise<T>> = [];
// 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<void>}
*/
async batchRecords(models: Model[]): Promise<void> {
async batchRecords(models: Model[], description: string): Promise<void> {
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<Model[]>}
*/
async handleRecords({buildKeyRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues = [], tableName, prepareRecordsOnly = true}: HandleRecordsArgs): Promise<Model[]> {
async handleRecords<T extends Model>({buildKeyRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues = [], tableName, prepareRecordsOnly = true}: HandleRecordsArgs<T>, description: string): Promise<T[]> {
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<T>({
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<T>({
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;

View file

@ -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');
});
});

View file

@ -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<CategoryModel[]>;
}
const CategoryHandler = (superclass: any) => class extends superclass {
const CategoryHandler = <TBase extends Constructor<ServerDataOperatorBase>>(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<CategoryChannelModel[]>}
*/
handleCategoryChannels = async ({categoryChannels, prepareRecordsOnly = true}: HandleCategoryChannelArgs): Promise<CategoryModel[]> => {
handleCategoryChannels = async ({categoryChannels, prepareRecordsOnly = true}: HandleCategoryChannelArgs): Promise<CategoryChannelModel[]> => {
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');
};
};

View file

@ -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');
});
});

View file

@ -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<MyChannelModel[]>;
}
const ChannelHandler = (superclass: any) => class extends superclass {
const ChannelHandler = <TBase extends Constructor<ServerDataOperatorBase>>(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');
};
};

View file

@ -49,6 +49,6 @@ describe('*** Operator: Group Handlers tests ***', () => {
tableName: MM_TABLES.SERVER.GROUP,
prepareRecordsOnly: false,
transformer: transformGroupRecord,
});
}, 'handleGroups');
});
});

View file

@ -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<GroupTeamModel[]>;
}
const GroupHandler = (superclass: any) => class extends superclass implements GroupHandlerMix {
const GroupHandler = <TBase extends Constructor<ServerDataOperatorBase>>(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;

View file

@ -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);
});
});

View file

@ -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<RoleModel[]>;
}, 'handleRole') as Promise<RoleModel[]>;
};
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<CustomEmojiModel[]>;
}, 'handleCustomEmojis') as Promise<CustomEmojiModel[]>;
};
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<SystemModel[]>;
}, 'handleSystem') as Promise<SystemModel[]>;
};
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<Array<(ReactionModel | CustomEmojiModel)>>}
*/
handleReactions = async ({postsReactions, prepareRecordsOnly, skipSync}: HandleReactionsArgs): Promise<ReactionModel[]> => {
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<Model>} execute.recordOperator
* @returns {Promise<void>}
*/
async execute({createRaws, transformer, tableName, updateRaws}: OperationArgs): Promise<Model[]> {
async execute<T extends Model>({createRaws, transformer, tableName, updateRaws}: OperationArgs<T>, description: string): Promise<T[]> {
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;

View file

@ -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 () => {

View file

@ -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<Model[]>;
handlePostsInChannel: (posts: Post[]) => Promise<void>;
handlePostsInThread: (rootPosts: PostsInThread[]) => Promise<void>;
handleReceivedPostsInChannel: (posts: Post[], prepareRecordsOnly?: boolean) => Promise<PostsInChannelModel[]>;
handleReceivedPostsInChannelSince: (posts: Post[], prepareRecordsOnly?: boolean) => Promise<PostsInChannelModel[]>;
handleReceivedPostsInChannelBefore: (posts: Post[], prepareRecordsOnly?: boolean) => Promise<PostsInChannelModel[]>;
handleReceivedPostsInChannelAfter: (posts: Post[], prepareRecordsOnly?: boolean) => Promise<PostsInChannelModel[]>;
handleReceivedPostForChannel: (post: Post, prepareRecordsOnly?: boolean) => Promise<PostsInChannelModel[]>;
handleReceivedPostsInThread: (postsMap: Record<string, Post[]>, prepareRecordsOnly?: boolean) => Promise<PostsInThreadModel[]>;
}
const PostHandler = (superclass: any) => class extends superclass {
const PostHandler = <TBase extends Constructor<ServerDataOperatorBase>>(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<PostsInChannelModel[]> => {
// 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<PostsInChannelModel[]> => {
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<PostsInChannelModel[]> => {
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<PostsInChannelModel[]> => {
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<PostsInChannelModel[]> => {
throw new Error(`handleReceivedPostsInChannelAfter Not implemented yet. posts count${posts.length} prepareRecordsOnly=${prepareRecordsOnly}`);
};
handleReceivedPostForChannel = async (posts: Post[], prepareRecordsOnly = false): Promise<PostsInChannelModel[]> => {
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<string, Post[]>, prepareRecordsOnly = false): Promise<PostsInThreadModel[]> => {
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;

View file

@ -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<PostsInChannelModel[]>;
handleReceivedPostsInChannelSince: (posts: Post[], prepareRecordsOnly?: boolean) => Promise<PostsInChannelModel[]>;
handleReceivedPostsInChannelBefore: (posts: Post[], prepareRecordsOnly?: boolean) => Promise<PostsInChannelModel[]>;
handleReceivedPostsInChannelAfter: (posts: Post[], prepareRecordsOnly?: boolean) => Promise<PostsInChannelModel[]>;
handleReceivedPostForChannel: (post: Post, prepareRecordsOnly?: boolean) => Promise<PostsInChannelModel[]>;
}
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<PostsInChannelModel[]> => {
// 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<PostsInChannelModel[]> => {
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<PostsInChannelModel[]> => {
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<PostsInChannelModel[]> => {
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<PostsInChannelModel[]> => {
throw new Error(`handleReceivedPostsInChannelAfter Not implemented yet. posts count${posts.length} prepareRecordsOnly=${prepareRecordsOnly}`);
};
handleReceivedPostForChannel = async (posts: Post[], prepareRecordsOnly = false): Promise<PostsInChannelModel[]> => {
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;

View file

@ -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<string, Post[]>, prepareRecordsOnly?: boolean) => Promise<PostsInThreadModel[]>;
}
const {POSTS_IN_THREAD} = Database.MM_TABLES.SERVER;
const PostsInThreadHandler = (superclass: any) => class extends superclass {
handleReceivedPostsInThread = async (postsMap: Record<string, Post[]>, prepareRecordsOnly = false): Promise<PostsInThreadModel[]> => {
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;

View file

@ -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<Array<ReactionModel | CustomEmojiModel>>;
}
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<Array<(ReactionModel | CustomEmojiModel)>>}
*/
handleReactions = async ({postsReactions, prepareRecordsOnly, skipSync}: HandleReactionsArgs): Promise<ReactionModel[]> => {
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;

View file

@ -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');
});
});

View file

@ -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<MyTeamModel[]>;
}
const TeamHandler = (superclass: any) => class extends superclass {
const TeamHandler = <TBase extends Constructor<ServerDataOperatorBase>>(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');
};
};

View file

@ -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 = <TBase extends Constructor<ServerDataOperatorBase>>(superclass: TBase) => class extends superclass {
handleTeamThreadsSync = async ({data, prepareRecordsOnly = false}: HandleTeamThreadsSyncArgs): Promise<TeamThreadsSyncModel[]> => {
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;

View file

@ -59,7 +59,7 @@ describe('*** Operator: Thread Handlers tests ***', () => {
createOrUpdateRawValues: threads,
tableName: 'Thread',
prepareRecordsOnly: true,
});
}, 'handleThreads(NEVER)');
// Should handle participants
expect(spyOnHandleThreadParticipants).toHaveBeenCalledWith({

View file

@ -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<Model[]>;
handleThreadParticipants: ({threadsParticipants, prepareRecordsOnly}: HandleThreadParticipantsArgs) => Promise<ThreadParticipantModel[]>;
handleThreadInTeam: ({threadsMap, prepareRecordsOnly}: HandleThreadInTeamArgs) => Promise<ThreadInTeamModel[]>;
}
const ThreadHandler = (superclass: any) => class extends superclass {
const ThreadHandler = <TBase extends Constructor<ServerDataOperatorBase>>(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<ThreadInTeamModel[]> => {
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<ThreadInTeamModel>(THREADS_IN_TEAM).query(
Q.where('team_id', teamId),
Q.where('id', Q.oneOf(threadIds)),
).fetch();
const chunksMap = chunks.reduce((result: Record<string, ThreadInTeamModel>, 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;

View file

@ -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<ThreadInTeamModel[]>;
}
const {THREADS_IN_TEAM} = MM_TABLES.SERVER;
const ThreadInTeamHandler = (superclass: any) => class extends superclass {
handleThreadInTeam = async ({threadsMap, prepareRecordsOnly = false}: HandleThreadInTeamArgs): Promise<ThreadInTeamModel[]> => {
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<ThreadInTeamModel>(THREADS_IN_TEAM).query(
Q.where('team_id', teamId),
Q.where('id', Q.oneOf(threadIds)),
).fetch();
const chunksMap = chunks.reduce((result: Record<string, ThreadInTeamModel>, 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;

View file

@ -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)');
});
});

View file

@ -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<UserModel[]>;
}
const UserHandler = (superclass: any) => class extends superclass {
const UserHandler = <TBase extends Constructor<ServerDataOperatorBase>>(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');
};
};

View file

@ -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,
) {

View file

@ -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<Model[]>}
*/
export const retrieveRecords = ({database, tableName, condition}: RetrieveRecordsArgs) => {
return database.collections.get(tableName).query(condition).fetch();
export const retrieveRecords = <T extends Model>({database, tableName, condition}: RetrieveRecordsArgs) => {
return database.collections.get<T>(tableName).query(condition).fetch();
};

View file

@ -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();

View file

@ -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) {

View file

@ -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) {

View file

@ -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};

View file

@ -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) {

View file

@ -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<T> {
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 = <T>(superclass: T) => new MixinBuilder(superclass);
export default mix;

View file

@ -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

View file

@ -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();

View file

@ -55,12 +55,12 @@ export type PrepareBaseRecordArgs = TransformerArgs & {
fieldsMapper: (model: Model) => void;
}
export type OperationArgs = {
export type OperationArgs<T extends Model> = {
tableName: string;
createRaws?: RecordPair[];
updateRaws?: RecordPair[];
deleteRaws?: Model[];
transformer: (args: TransformerArgs) => Promise<Model>;
deleteRaws?: T[];
transformer: (args: TransformerArgs) => Promise<T>;
};
export type Models = Array<Class<Model>>;
@ -157,10 +157,10 @@ export type ProcessRecordsArgs = {
buildKeyRecordBy?: (obj: Record<string, any>) => string;
};
export type HandleRecordsArgs = {
export type HandleRecordsArgs<T extends Model> = {
buildKeyRecordBy?: (obj: Record<string, any>) => string;
fieldName: string;
transformer: (args: TransformerArgs) => Promise<Model>;
transformer: (args: TransformerArgs) => Promise<T>;
createOrUpdateRawValues: RawValue[];
deleteRawValues?: RawValue[];
tableName: string;
@ -311,8 +311,8 @@ export type GetDatabaseConnectionArgs = {
setAsActiveDatabase: boolean;
}
export type ProcessRecordResults = {
export type ProcessRecordResults<T extends Model> = {
createRaws: RecordPair[];
updateRaws: RecordPair[];
deleteRaws: Model[];
deleteRaws: T[];
}

3
types/utils/mixins.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
type Constructor<T={}> = new (...args: any[]) => T;