diff --git a/app/database/admin/data_operator/comparators/index.ts b/app/database/admin/data_operator/comparators/index.ts index a498343a66..a7887b7560 100644 --- a/app/database/admin/data_operator/comparators/index.ts +++ b/app/database/admin/data_operator/comparators/index.ts @@ -1,26 +1,36 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {App} from '@database/default/models'; -import {Role, User} from '@database/server/models'; +import App from '@typings/database/app'; +import Channel from '@typings/database/channel'; +import ChannelInfo from '@typings/database/channel_info'; import ChannelMembership from '@typings/database/channel_membership'; import CustomEmoji from '@typings/database/custom_emoji'; import { RawApp, + RawChannel, + RawChannelInfo, RawChannelMembership, RawCustomEmoji, RawDraft, RawGlobal, RawGroup, RawGroupMembership, - RawGroupsInTeam, RawGroupsInChannel, + RawGroupsInTeam, + RawMyChannel, + RawMyChannelSettings, + RawMyTeam, RawPost, RawPreference, RawRole, RawServers, + RawSlashCommand, RawSystem, + RawTeam, + RawTeamChannelHistory, RawTeamMembership, + RawTeamSearchHistory, RawTermsOfService, RawUser, } from '@typings/database/database'; @@ -30,12 +40,21 @@ import Group from '@typings/database/group'; import GroupMembership from '@typings/database/group_membership'; import GroupsInChannel from '@typings/database/groups_in_channel'; import GroupsInTeam from '@typings/database/groups_in_team'; +import MyChannel from '@typings/database/my_channel'; +import MyChannelSettings from '@typings/database/my_channel_settings'; +import MyTeam from '@typings/database/my_team'; import Post from '@typings/database/post'; import Preference from '@typings/database/preference'; +import Role from '@typings/database/role'; import Servers from '@typings/database/servers'; +import SlashCommand from '@typings/database/slash_command'; import System from '@typings/database/system'; +import Team from '@typings/database/team'; +import TeamChannelHistory from '@typings/database/team_channel_history'; import TeamMembership from '@typings/database/team_membership'; +import TeamSearchHistory from '@typings/database/team_search_history'; import TermsOfService from '@typings/database/terms_of_service'; +import User from '@typings/database/user'; /** * This file contains all the comparators that are used by the handlers to find out which records to truly update and @@ -46,9 +65,9 @@ import TermsOfService from '@typings/database/terms_of_service'; export const isRecordAppEqualToRaw = (record: App, raw: RawApp) => { return ( - raw.buildNumber === record.buildNumber && - raw.createdAt === record.createdAt && - raw.versionNumber === record.versionNumber + raw.build_number === record.buildNumber && + raw.created_at === record.createdAt && + raw.version_number === record.versionNumber ); }; @@ -57,19 +76,19 @@ export const isRecordGlobalEqualToRaw = (record: Global, raw: RawGlobal) => { }; export const isRecordServerEqualToRaw = (record: Servers, raw: RawServers) => { - return raw.url === record.url && raw.dbPath === record.dbPath; + return raw.url === record.url && raw.db_path === record.dbPath; }; export const isRecordRoleEqualToRaw = (record: Role, raw: RawRole) => { - return raw.name === record.name && JSON.stringify(raw.permissions) === JSON.stringify(record.permissions); + return raw.id === record.id; }; export const isRecordSystemEqualToRaw = (record: System, raw: RawSystem) => { - return raw.name === record.name && raw.value === record.value; + return raw.id === record.id; }; export const isRecordTermsOfServiceEqualToRaw = (record: TermsOfService, raw: RawTermsOfService) => { - return raw.acceptedAt === record.acceptedAt; + return raw.id === record.id; }; export const isRecordDraftEqualToRaw = (record: Draft, raw: RawDraft) => { @@ -88,8 +107,7 @@ export const isRecordPreferenceEqualToRaw = (record: Preference, raw: RawPrefere return ( raw.category === record.category && raw.name === record.name && - raw.user_id === record.userId && - raw.value === record.value + raw.user_id === record.userId ); }; @@ -98,7 +116,7 @@ export const isRecordTeamMembershipEqualToRaw = (record: TeamMembership, raw: Ra }; export const isRecordCustomEmojiEqualToRaw = (record: CustomEmoji, raw: RawCustomEmoji) => { - return raw.name === record.name; + return raw.id === record.id; }; export const isRecordGroupMembershipEqualToRaw = (record: GroupMembership, raw: RawGroupMembership) => { @@ -110,7 +128,7 @@ export const isRecordChannelMembershipEqualToRaw = (record: ChannelMembership, r }; export const isRecordGroupEqualToRaw = (record: Group, raw: RawGroup) => { - return raw.name === record.name && raw.display_name === record.displayName; + return raw.id === record.id; }; export const isRecordGroupsInTeamEqualToRaw = (record: GroupsInTeam, raw: RawGroupsInTeam) => { @@ -120,3 +138,39 @@ export const isRecordGroupsInTeamEqualToRaw = (record: GroupsInTeam, raw: RawGro export const isRecordGroupsInChannelEqualToRaw = (record: GroupsInChannel, raw: RawGroupsInChannel) => { return raw.channel_id === record.channelId && raw.group_id === record.groupId; }; + +export const isRecordTeamEqualToRaw = (record: Team, raw: RawTeam) => { + return raw.id === record.id; +}; + +export const isRecordTeamChannelHistoryEqualToRaw = (record: TeamChannelHistory, raw: RawTeamChannelHistory) => { + return raw.team_id === record.teamId; +}; + +export const isRecordTeamSearchHistoryEqualToRaw = (record: TeamSearchHistory, raw: RawTeamSearchHistory) => { + return raw.team_id === record.teamId && raw.term === record.term; +}; + +export const isRecordSlashCommandEqualToRaw = (record: SlashCommand, raw: RawSlashCommand) => { + return raw.id === record.id; +}; + +export const isRecordMyTeamEqualToRaw = (record: MyTeam, raw: RawMyTeam) => { + return raw.team_id === record.teamId; +}; + +export const isRecordChannelEqualToRaw = (record: Channel, raw: RawChannel) => { + return raw.id === record.id; +}; + +export const isRecordMyChannelSettingsEqualToRaw = (record: MyChannelSettings, raw: RawMyChannelSettings) => { + return raw.channel_id === record.channelId; +}; + +export const isRecordChannelInfoEqualToRaw = (record: ChannelInfo, raw: RawChannelInfo) => { + return raw.channel_id === record.channelId; +}; + +export const isRecordMyChannelEqualToRaw = (record: MyChannel, raw: RawMyChannel) => { + return raw.channel_id === record.channelId; +}; diff --git a/app/database/admin/data_operator/handlers/index.ts b/app/database/admin/data_operator/handlers/index.ts index 96d24ff353..70ef809a7a 100644 --- a/app/database/admin/data_operator/handlers/index.ts +++ b/app/database/admin/data_operator/handlers/index.ts @@ -7,6 +7,8 @@ import Model from '@nozbe/watermelondb/Model'; import {MM_TABLES} from '@constants/database'; import { isRecordAppEqualToRaw, + isRecordChannelEqualToRaw, + isRecordChannelInfoEqualToRaw, isRecordChannelMembershipEqualToRaw, isRecordCustomEmojiEqualToRaw, isRecordDraftEqualToRaw, @@ -15,12 +17,19 @@ import { isRecordGroupMembershipEqualToRaw, isRecordGroupsInChannelEqualToRaw, isRecordGroupsInTeamEqualToRaw, + isRecordMyChannelEqualToRaw, + isRecordMyChannelSettingsEqualToRaw, + isRecordMyTeamEqualToRaw, isRecordPostEqualToRaw, isRecordPreferenceEqualToRaw, isRecordRoleEqualToRaw, isRecordServerEqualToRaw, + isRecordSlashCommandEqualToRaw, isRecordSystemEqualToRaw, + isRecordTeamChannelHistoryEqualToRaw, + isRecordTeamEqualToRaw, isRecordTeamMembershipEqualToRaw, + isRecordTeamSearchHistoryEqualToRaw, isRecordTermsOfServiceEqualToRaw, isRecordUserEqualToRaw, } from '@database/admin/data_operator/comparators'; @@ -40,6 +49,8 @@ import { PrepareForDatabaseArgs, PrepareRecordsArgs, ProcessInputsArgs, + RawChannel, + RawChannelInfo, RawChannelMembership, RawCustomEmoji, RawDraft, @@ -49,12 +60,19 @@ import { RawGroupMembership, RawGroupsInChannel, RawGroupsInTeam, + RawMyChannel, + RawMyChannelSettings, + RawMyTeam, RawPost, RawPostMetadata, RawPostsInThread, RawPreference, RawReaction, + RawSlashCommand, + RawTeam, + RawTeamChannelHistory, RawTeamMembership, + RawTeamSearchHistory, RawUser, RawValue, } from '@typings/database/database'; @@ -70,7 +88,9 @@ import DataOperatorException from '../../exceptions/data_operator_exception'; import DatabaseConnectionException from '../../exceptions/database_connection_exception'; import { operateAppRecord, + operateChannelInfoRecord, operateChannelMembershipRecord, + operateChannelRecord, operateCustomEmojiRecord, operateDraftRecord, operateFileRecord, @@ -79,6 +99,9 @@ import { operateGroupRecord, operateGroupsInChannelRecord, operateGroupsInTeamRecord, + operateMyChannelRecord, + operateMyChannelSettingsRecord, + operateMyTeamRecord, operatePostInThreadRecord, operatePostMetadataRecord, operatePostRecord, @@ -87,8 +110,12 @@ import { operateReactionRecord, operateRoleRecord, operateServersRecord, + operateSlashCommandRecord, operateSystemRecord, + operateTeamChannelHistoryRecord, operateTeamMembershipRecord, + operateTeamRecord, + operateTeamSearchHistoryRecord, operateTermsOfServiceRecord, operateUserRecord, } from '../operators'; @@ -96,6 +123,7 @@ import { createPostsChain, getRangeOfValues, getRawRecordPairs, + getUniqueRawsBy, hasSimilarUpdateAt, retrieveRecords, sanitizePosts, @@ -103,6 +131,8 @@ import { } from '../utils'; const { + CHANNEL, + CHANNEL_INFO, CHANNEL_MEMBERSHIP, CUSTOM_EMOJI, DRAFT, @@ -111,18 +141,23 @@ const { GROUPS_IN_CHANNEL, GROUPS_IN_TEAM, GROUP_MEMBERSHIP, + MY_CHANNEL, + MY_CHANNEL_SETTINGS, + MY_TEAM, POST, POSTS_IN_CHANNEL, POSTS_IN_THREAD, POST_METADATA, PREFERENCE, REACTION, + SLASH_COMMAND, + TEAM, + TEAM_CHANNEL_HISTORY, TEAM_MEMBERSHIP, + TEAM_SEARCH_HISTORY, USER, } = MM_TABLES.SERVER; -// TODO : We assume that we are receiving clean&correct data from the server. Hence, we can never have two posts in an array with the same post_id. This should be confirmed. - class DataOperator { /** * serverDatabase : In a multi-server configuration, this connection will be used by WebSockets and other parties to update databases other than the active one. @@ -143,9 +178,10 @@ class DataOperator { * @returns {Promise} */ handleIsolatedEntity = async ({tableName, values}: HandleIsolatedEntityArgs) => { - let comparator; + let findMatchingRecordBy; let fieldName; let operator; + let rawValues; if (!values.length) { throw new DataOperatorException( @@ -155,45 +191,52 @@ class DataOperator { switch (tableName) { case IsolatedEntities.APP: { - comparator = isRecordAppEqualToRaw; + findMatchingRecordBy = isRecordAppEqualToRaw; fieldName = 'version_number'; operator = operateAppRecord; + rawValues = getUniqueRawsBy({raws: values, key: 'versionNumber'}); break; } case IsolatedEntities.CUSTOM_EMOJI: { - comparator = isRecordCustomEmojiEqualToRaw; - fieldName = 'name'; + findMatchingRecordBy = isRecordCustomEmojiEqualToRaw; + fieldName = 'id'; operator = operateCustomEmojiRecord; + rawValues = getUniqueRawsBy({raws: values, key: 'id'}); break; } case IsolatedEntities.GLOBAL: { - comparator = isRecordGlobalEqualToRaw; + findMatchingRecordBy = isRecordGlobalEqualToRaw; fieldName = 'name'; operator = operateGlobalRecord; + rawValues = getUniqueRawsBy({raws: values, key: 'name'}); break; } case IsolatedEntities.ROLE: { - comparator = isRecordRoleEqualToRaw; - fieldName = 'name'; + findMatchingRecordBy = isRecordRoleEqualToRaw; + fieldName = 'id'; operator = operateRoleRecord; + rawValues = getUniqueRawsBy({raws: values, key: 'id'}); break; } case IsolatedEntities.SERVERS: { - comparator = isRecordServerEqualToRaw; - fieldName = 'db_path'; + findMatchingRecordBy = isRecordServerEqualToRaw; + fieldName = 'url'; operator = operateServersRecord; + rawValues = getUniqueRawsBy({raws: values, key: 'displayName'}); break; } case IsolatedEntities.SYSTEM: { - comparator = isRecordSystemEqualToRaw; - fieldName = 'name'; + findMatchingRecordBy = isRecordSystemEqualToRaw; + fieldName = 'id'; operator = operateSystemRecord; + rawValues = getUniqueRawsBy({raws: values, key: 'id'}); break; } case IsolatedEntities.TERMS_OF_SERVICE: { - comparator = isRecordTermsOfServiceEqualToRaw; - fieldName = 'accepted_at'; + findMatchingRecordBy = isRecordTermsOfServiceEqualToRaw; + fieldName = 'id'; operator = operateTermsOfServiceRecord; + rawValues = getUniqueRawsBy({raws: values, key: 'id'}); break; } default: { @@ -203,12 +246,12 @@ class DataOperator { } } - if (operator && fieldName && comparator) { + if (operator && fieldName && findMatchingRecordBy) { await this.handleEntityRecords({ - comparator, + findMatchingRecordBy, fieldName, operator, - rawValues: values, + rawValues, tableName, }); } @@ -226,11 +269,13 @@ class DataOperator { ); } + const rawValues = getUniqueRawsBy({raws: drafts, key: 'channel_id'}); + await this.handleEntityRecords({ - comparator: isRecordDraftEqualToRaw, + findMatchingRecordBy: isRecordDraftEqualToRaw, fieldName: 'channel_id', operator: operateDraftRecord, - rawValues: drafts, + rawValues, tableName: DRAFT, }); }; @@ -250,6 +295,8 @@ class DataOperator { ); } + const rawValues = getUniqueRawsBy({raws: reactions, key: 'emoji_name'}) as RawReaction[]; + const database = await this.getDatabase(REACTION); const { @@ -259,7 +306,7 @@ class DataOperator { } = await sanitizeReactions({ database, post_id: reactions[0].post_id, - rawReactions: reactions, + rawReactions: rawValues, }); let batchRecords: Model[] = []; @@ -320,9 +367,11 @@ class DataOperator { ); } + const rawValues = getUniqueRawsBy({raws: values, key: 'id'}) as RawPost[]; + // By sanitizing the values, we are separating 'posts' that needs updating ( i.e. un-ordered posts ) from those that need to be created in our database const {postsOrdered, postsUnordered} = sanitizePosts({ - posts: values, + posts: rawValues, orders, }); @@ -330,7 +379,7 @@ class DataOperator { const futureEntries = await this.processInputs({ rawValues: postsOrdered, tableName, - comparator: isRecordPostEqualToRaw, + findMatchingRecordBy: isRecordPostEqualToRaw, fieldName: 'id', }); @@ -450,7 +499,7 @@ class DataOperator { if (postsUnordered.length) { // Truly update those posts that have a different update_at value await this.handleEntityRecords({ - comparator: isRecordPostEqualToRaw, + findMatchingRecordBy: isRecordPostEqualToRaw, fieldName: 'id', operator: operatePostRecord, rawValues: postsUnordered, @@ -720,11 +769,14 @@ class DataOperator { 'An empty "users" array has been passed to the handleUsers method', ); } + + const rawValues = getUniqueRawsBy({raws: users, key: 'id'}); + await this.handleEntityRecords({ - comparator: isRecordUserEqualToRaw, + findMatchingRecordBy: isRecordUserEqualToRaw, fieldName: 'id', operator: operateUserRecord, - rawValues: users, + rawValues, tableName: USER, }); }; @@ -742,11 +794,13 @@ class DataOperator { ); } + const rawValues = getUniqueRawsBy({raws: preferences, key: 'name'}); + await this.handleEntityRecords({ - comparator: isRecordPreferenceEqualToRaw, + findMatchingRecordBy: isRecordPreferenceEqualToRaw, fieldName: 'user_id', operator: operatePreferenceRecord, - rawValues: preferences, + rawValues, tableName: PREFERENCE, }); }; @@ -763,11 +817,14 @@ class DataOperator { 'An empty "teamMemberships" array has been passed to the handleTeamMemberships method', ); } + + const rawValues = getUniqueRawsBy({raws: teamMemberships, key: 'team_id'}); + await this.handleEntityRecords({ - comparator: isRecordTeamMembershipEqualToRaw, + findMatchingRecordBy: isRecordTeamMembershipEqualToRaw, fieldName: 'user_id', operator: operateTeamMembershipRecord, - rawValues: teamMemberships, + rawValues, tableName: TEAM_MEMBERSHIP, }); }; @@ -785,11 +842,13 @@ class DataOperator { ); } + const rawValues = getUniqueRawsBy({raws: groupMemberships, key: 'group_id'}); + await this.handleEntityRecords({ - comparator: isRecordGroupMembershipEqualToRaw, + findMatchingRecordBy: isRecordGroupMembershipEqualToRaw, fieldName: 'user_id', operator: operateGroupMembershipRecord, - rawValues: groupMemberships, + rawValues, tableName: GROUP_MEMBERSHIP, }); }; @@ -807,11 +866,13 @@ class DataOperator { ); } + const rawValues = getUniqueRawsBy({raws: channelMemberships, key: 'channel_id'}); + await this.handleEntityRecords({ - comparator: isRecordChannelMembershipEqualToRaw, + findMatchingRecordBy: isRecordChannelMembershipEqualToRaw, fieldName: 'user_id', operator: operateChannelMembershipRecord, - rawValues: channelMemberships, + rawValues, tableName: CHANNEL_MEMBERSHIP, }); }; @@ -829,70 +890,292 @@ class DataOperator { ); } + const rawValues = getUniqueRawsBy({raws: groups, key: 'name'}); + await this.handleEntityRecords({ - comparator: isRecordGroupEqualToRaw, + findMatchingRecordBy: isRecordGroupEqualToRaw, fieldName: 'name', operator: operateGroupRecord, - rawValues: groups, + rawValues, tableName: GROUP, }); }; /** * handleGroupsInTeam: Handler responsible for the Create/Update operations occurring on the GROUPS_IN_TEAM entity from the 'Server' schema - * @param {RawGroupsInTeam[]} groupsInTeam + * @param {RawGroupsInTeam[]} groupsInTeams * @throws DataOperatorException * @returns {Promise} */ - handleGroupsInTeam = async (groupsInTeam: RawGroupsInTeam[]) => { - if (!groupsInTeam.length) { + handleGroupsInTeam = async (groupsInTeams: RawGroupsInTeam[]) => { + if (!groupsInTeams.length) { throw new DataOperatorException( 'An empty "groups" array has been passed to the handleGroupsInTeam method', ); } + const rawValues = getUniqueRawsBy({raws: groupsInTeams, key: 'group_id'}); + await this.handleEntityRecords({ - comparator: isRecordGroupsInTeamEqualToRaw, + findMatchingRecordBy: isRecordGroupsInTeamEqualToRaw, fieldName: 'group_id', operator: operateGroupsInTeamRecord, - rawValues: groupsInTeam, + rawValues, tableName: GROUPS_IN_TEAM, }); }; /** * handleGroupsInChannel: Handler responsible for the Create/Update operations occurring on the GROUPS_IN_CHANNEL entity from the 'Server' schema - * @param {RawGroupsInChannel[]} groupsInChannel + * @param {RawGroupsInChannel[]} groupsInChannels * @throws DataOperatorException * @returns {Promise} */ - handleGroupsInChannel = async (groupsInChannel: RawGroupsInChannel[]) => { - if (!groupsInChannel.length) { + handleGroupsInChannel = async (groupsInChannels: RawGroupsInChannel[]) => { + if (!groupsInChannels.length) { throw new DataOperatorException( 'An empty "groups" array has been passed to the handleGroupsInTeam method', ); } + const rawValues = getUniqueRawsBy({raws: groupsInChannels, key: 'channel_id'}); + await this.handleEntityRecords({ - comparator: isRecordGroupsInChannelEqualToRaw, + findMatchingRecordBy: isRecordGroupsInChannelEqualToRaw, fieldName: 'group_id', operator: operateGroupsInChannelRecord, - rawValues: groupsInChannel, + rawValues, tableName: GROUPS_IN_CHANNEL, }); }; + /** + * handleTeam: Handler responsible for the Create/Update operations occurring on the TEAM entity from the 'Server' schema + * @param {RawTeam[]} teams + * @throws DataOperatorException + * @returns {Promise} + */ + handleTeam = async (teams: RawTeam[]) => { + if (!teams.length) { + throw new DataOperatorException( + 'An empty "teams" array has been passed to the handleTeam method', + ); + } + + const rawValues = getUniqueRawsBy({raws: teams, key: 'id'}); + + await this.handleEntityRecords({ + findMatchingRecordBy: isRecordTeamEqualToRaw, + fieldName: 'id', + operator: operateTeamRecord, + rawValues, + tableName: TEAM, + }); + }; + + /** + * handleTeamChannelHistory: Handler responsible for the Create/Update operations occurring on the TEAM_CHANNEL_HISTORY entity from the 'Server' schema + * @param {RawTeamChannelHistory[]} teamChannelHistories + * @throws DataOperatorException + * @returns {Promise} + */ + handleTeamChannelHistory = async (teamChannelHistories: RawTeamChannelHistory[]) => { + if (!teamChannelHistories.length) { + throw new DataOperatorException( + 'An empty "teamChannelHistories" array has been passed to the handleTeamChannelHistory method', + ); + } + + const rawValues = getUniqueRawsBy({raws: teamChannelHistories, key: 'team_id'}); + + await this.handleEntityRecords({ + findMatchingRecordBy: isRecordTeamChannelHistoryEqualToRaw, + fieldName: 'team_id', + operator: operateTeamChannelHistoryRecord, + rawValues, + tableName: TEAM_CHANNEL_HISTORY, + }); + }; + + /** + * handleTeamSearchHistory: Handler responsible for the Create/Update operations occurring on the TEAM_SEARCH_HISTORY entity from the 'Server' schema + * @param {RawTeamSearchHistory[]} teamSearchHistories + * @throws DataOperatorException + * @returns {Promise} + */ + handleTeamSearchHistory = async (teamSearchHistories: RawTeamSearchHistory[]) => { + if (!teamSearchHistories.length) { + throw new DataOperatorException( + 'An empty "teamSearchHistories" array has been passed to the handleTeamSearchHistory method', + ); + } + + const rawValues = getUniqueRawsBy({raws: teamSearchHistories, key: 'term'}); + + await this.handleEntityRecords({ + findMatchingRecordBy: isRecordTeamSearchHistoryEqualToRaw, + fieldName: 'team_id', + operator: operateTeamSearchHistoryRecord, + rawValues, + tableName: TEAM_SEARCH_HISTORY, + }); + }; + + /** + * handleSlashCommand: Handler responsible for the Create/Update operations occurring on the SLASH_COMMAND entity from the 'Server' schema + * @param {RawSlashCommand[]} slashCommands + * @throws DataOperatorException + * @returns {Promise} + */ + handleSlashCommand = async (slashCommands: RawSlashCommand[]) => { + if (!slashCommands.length) { + throw new DataOperatorException( + 'An empty "slashCommands" array has been passed to the handleSlashCommand method', + ); + } + + const rawValues = getUniqueRawsBy({raws: slashCommands, key: 'id'}); + + await this.handleEntityRecords({ + findMatchingRecordBy: isRecordSlashCommandEqualToRaw, + fieldName: 'id', + operator: operateSlashCommandRecord, + rawValues, + tableName: SLASH_COMMAND, + }); + }; + + /** + * handleMyTeam: Handler responsible for the Create/Update operations occurring on the MY_TEAM entity from the 'Server' schema + * @param {RawMyTeam[]} myTeams + * @throws DataOperatorException + * @returns {Promise} + */ + handleMyTeam = async (myTeams: RawMyTeam[]) => { + if (!myTeams.length) { + throw new DataOperatorException( + 'An empty "myTeams" array has been passed to the handleSlashCommand method', + ); + } + + const rawValues = getUniqueRawsBy({raws: myTeams, key: 'team_id'}); + + await this.handleEntityRecords({ + findMatchingRecordBy: isRecordMyTeamEqualToRaw, + fieldName: 'team_id', + operator: operateMyTeamRecord, + rawValues, + tableName: MY_TEAM, + }); + }; + + /** + * handleChannel: Handler responsible for the Create/Update operations occurring on the CHANNEL entity from the 'Server' schema + * @param {RawChannel[]} channels + * @throws DataOperatorException + * @returns {Promise} + */ + handleChannel = async (channels: RawChannel[]) => { + if (!channels.length) { + throw new DataOperatorException( + 'An empty "channels" array has been passed to the handleChannel method', + ); + } + + const rawValues = getUniqueRawsBy({raws: channels, key: 'id'}); + + await this.handleEntityRecords({ + findMatchingRecordBy: isRecordChannelEqualToRaw, + fieldName: 'id', + operator: operateChannelRecord, + rawValues, + tableName: CHANNEL, + }); + }; + + /** + * handleMyChannelSettings: Handler responsible for the Create/Update operations occurring on the MY_CHANNEL_SETTINGS entity from the 'Server' schema + * @param {RawMyChannelSettings[]} settings + * @throws DataOperatorException + * @returns {Promise} + */ + handleMyChannelSettings = async (settings: RawMyChannelSettings[]) => { + if (!settings.length) { + throw new DataOperatorException( + 'An empty "settings" array has been passed to the handleMyChannelSettings method', + ); + } + + const rawValues = getUniqueRawsBy({raws: settings, key: 'channel_id'}); + + await this.handleEntityRecords({ + findMatchingRecordBy: isRecordMyChannelSettingsEqualToRaw, + fieldName: 'channel_id', + operator: operateMyChannelSettingsRecord, + rawValues, + tableName: MY_CHANNEL_SETTINGS, + }); + }; + + /** + * handleChannelInfo: Handler responsible for the Create/Update operations occurring on the CHANNEL_INFO entity from the 'Server' schema + * @param {RawChannelInfo[]} channelInfos + * @throws DataOperatorException + * @returns {Promise} + */ + handleChannelInfo = async (channelInfos: RawChannelInfo[]) => { + if (!channelInfos.length) { + throw new DataOperatorException( + 'An empty "channelInfos" array has been passed to the handleMyChannelSettings method', + ); + } + + const rawValues = getUniqueRawsBy({raws: channelInfos, key: 'channel_id'}); + + await this.handleEntityRecords({ + findMatchingRecordBy: isRecordChannelInfoEqualToRaw, + fieldName: 'channel_id', + operator: operateChannelInfoRecord, + rawValues, + tableName: CHANNEL_INFO, + }); + }; + + /** + * handleMyChannel: Handler responsible for the Create/Update operations occurring on the MY_CHANNEL entity from the 'Server' schema + * @param {RawMyChannel[]} myChannels + * @throws DataOperatorException + * @returns {Promise} + */ + handleMyChannel = async (myChannels: RawMyChannel[]) => { + if (!myChannels.length) { + throw new DataOperatorException( + 'An empty "myChannels" array has been passed to the handleMyChannel method', + ); + } + + const rawValues = getUniqueRawsBy({raws: myChannels, key: 'channel_id'}); + + await this.handleEntityRecords({ + findMatchingRecordBy: isRecordMyChannelEqualToRaw, + fieldName: 'channel_id', + operator: operateMyChannelRecord, + rawValues, + tableName: MY_CHANNEL, + }); + }; + /** * handleEntityRecords : Utility that processes some entities' data against values already present in the database so as to avoid duplicity. - * @param {HandleEntityRecordsArgs} handleEntityRecords - * @param {(existing: Model, newElement: RawValue) => boolean} handleEntityRecords.comparator - * @param {string} handleEntityRecords.fieldName - * @param {(DataFactoryArgs) => Promise} handleEntityRecords.operator - * @param {RawValue[]} handleEntityRecords.rawValues - * @param {string} handleEntityRecords.tableName + * @param {HandleEntityRecordsArgs} handleEntityArgs + * @param {(existing: Model, newElement: RawValue) => boolean} handleEntityArgs.findMatchingRecordBy + * @param {string} handleEntityArgs.fieldName + * @param {(DataFactoryArgs) => Promise} handleEntityArgs.operator + * @param {RawValue[]} handleEntityArgs.rawValues + * @param {string} handleEntityArgs.tableName * @returns {Promise} */ - private handleEntityRecords = async ({comparator, fieldName, operator, rawValues, tableName}: HandleEntityRecordsArgs) => { + private handleEntityRecords = async ({findMatchingRecordBy, fieldName, operator, rawValues, tableName}: HandleEntityRecordsArgs) => { if (!rawValues.length) { return null; } @@ -900,7 +1183,7 @@ class DataOperator { const {createRaws, updateRaws} = await this.processInputs({ rawValues, tableName, - comparator, + findMatchingRecordBy, fieldName, }); @@ -914,17 +1197,16 @@ class DataOperator { return records; }; - // TODO : Add jest to processInputs /** * processInputs: This method weeds out duplicates entries. It may happen that we do multiple inserts for * the same value. Hence, prior to that we query the database and pick only those values that are 'new' from the 'Raw' array. - * @param {ProcessInputsArgs} prepareRecords - * @param {RawValue[]} prepareRecords.rawValues - * @param {string} prepareRecords.tableName - * @param {string} prepareRecords.fieldName - * @param {(existing: Model, newElement: RawValue) => boolean} prepareRecords.comparator + * @param {ProcessInputsArgs} inputsArg + * @param {RawValue[]} inputsArg.rawValues + * @param {string} inputsArg.tableName + * @param {string} inputsArg.fieldName + * @param {(existing: Model, newElement: RawValue) => boolean} inputsArg.findMatchingRecordBy */ - private processInputs = async ({rawValues, tableName, comparator, fieldName}: ProcessInputsArgs) => { + private processInputs = async ({rawValues, tableName, findMatchingRecordBy, fieldName}: ProcessInputsArgs) => { // We will query an entity 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}); @@ -942,7 +1224,7 @@ class DataOperator { if (existingRecords.length > 0) { rawValues.map((newElement: RawValue) => { const findIndex = existingRecords.findIndex((existing) => { - return comparator(existing, newElement); + return findMatchingRecordBy(existing, newElement); }); // We found a record in the database that matches this element; hence, we'll proceed for an UPDATE operation diff --git a/app/database/admin/data_operator/handlers/test.ts b/app/database/admin/data_operator/handlers/test.ts index b36746d3eb..470af97d30 100644 --- a/app/database/admin/data_operator/handlers/test.ts +++ b/app/database/admin/data_operator/handlers/test.ts @@ -1,7 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import DatabaseManager from '@database/admin/database_manager'; import DataOperator from '@database/admin/data_operator'; import { isRecordAppEqualToRaw, @@ -12,16 +11,37 @@ import { isRecordSystemEqualToRaw, isRecordTermsOfServiceEqualToRaw, } from '@database/admin/data_operator/comparators'; +import DatabaseManager from '@database/admin/database_manager'; import DataOperatorException from '@database/admin/exceptions/data_operator_exception'; +import {RawApp, RawGlobal, RawRole, RawServers, RawTermsOfService} from '@typings/database/database'; import {DatabaseType, IsolatedEntities} from '@typings/database/enums'; + import { operateAppRecord, + operateChannelInfoRecord, + operateChannelMembershipRecord, + operateChannelRecord, + operateCustomEmojiRecord, operateDraftRecord, operateGlobalRecord, + operateGroupMembershipRecord, + operateGroupRecord, + operateGroupsInChannelRecord, + operateGroupsInTeamRecord, + operateMyChannelRecord, + operateMyChannelSettingsRecord, + operateMyTeamRecord, + operatePreferenceRecord, operateRoleRecord, operateServersRecord, + operateSlashCommandRecord, operateSystemRecord, + operateTeamChannelHistoryRecord, + operateTeamMembershipRecord, + operateTeamRecord, + operateTeamSearchHistoryRecord, operateTermsOfServiceRecord, + operateUserRecord, } from '../operators'; jest.mock('@database/admin/database_manager'); @@ -60,18 +80,16 @@ describe('*** DataOperator: Handlers tests ***', () => { const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords'); - const values = [ + const values: RawApp[] = [ { - buildNumber: 'build-10x', - createdAt: 1, - id: 'id-21', - versionNumber: 'version-10', + build_number: 'build-10x', + created_at: 1, + version_number: 'version-10', }, { - buildNumber: 'build-11y', - createdAt: 1, - id: 'id-22', - versionNumber: 'version-11', + build_number: 'build-11y', + created_at: 1, + version_number: 'version-11', }, ]; @@ -80,8 +98,14 @@ describe('*** DataOperator: Handlers tests ***', () => { expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({ fieldName: 'version_number', operator: operateAppRecord, - comparator: isRecordAppEqualToRaw, - rawValues: values, + findMatchingRecordBy: isRecordAppEqualToRaw, + rawValues: [ + { + build_number: 'build-11y', + created_at: 1, + version_number: 'version-11', + }, + ], tableName: 'app', }); }); @@ -93,12 +117,12 @@ describe('*** DataOperator: Handlers tests ***', () => { expect(defaultDB).toBeTruthy(); const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords'); - const values = [{id: 'global-1-id', name: 'global-1-name', value: 'global-1-value'}]; + const values: RawGlobal[] = [{name: 'global-1-name', value: 'global-1-value'}]; await DataOperator.handleIsolatedEntity({tableName: IsolatedEntities.GLOBAL, values}); expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({ - comparator: isRecordGlobalEqualToRaw, + findMatchingRecordBy: isRecordGlobalEqualToRaw, fieldName: 'name', operator: operateGlobalRecord, rawValues: values, @@ -113,23 +137,30 @@ describe('*** DataOperator: Handlers tests ***', () => { expect(defaultDB).toBeTruthy(); const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords'); - const values = [ + const values: RawServers[] = [ { - dbPath: 'server.db', - displayName: 'community', - id: 'server-id-1', - mentionCount: 0, - unreadCount: 0, + db_path: 'server.db', + display_name: 'community', + mention_count: 0, + unread_count: 0, url: 'https://community.mattermost.com', }, ]; await DataOperator.handleIsolatedEntity({tableName: IsolatedEntities.SERVERS, values}); expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({ - comparator: isRecordServerEqualToRaw, - fieldName: 'db_path', + fieldName: 'url', operator: operateServersRecord, - rawValues: values, + findMatchingRecordBy: isRecordServerEqualToRaw, + rawValues: [ + { + db_path: 'server.db', + display_name: 'community', + mention_count: 0, + unread_count: 0, + url: 'https://community.mattermost.com', + }, + ], tableName: 'servers', }); }); @@ -141,7 +172,7 @@ describe('*** DataOperator: Handlers tests ***', () => { expect(database).toBeTruthy(); const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords'); - const values = [ + const values: RawRole[] = [ { id: 'custom-emoji-id-1', name: 'custom-emoji-1', @@ -155,10 +186,16 @@ describe('*** DataOperator: Handlers tests ***', () => { }); expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({ - comparator: isRecordRoleEqualToRaw, - fieldName: 'name', + fieldName: 'id', operator: operateRoleRecord, - rawValues: values, + findMatchingRecordBy: isRecordRoleEqualToRaw, + rawValues: [ + { + id: 'custom-emoji-id-1', + name: 'custom-emoji-1', + permissions: ['custom-emoji-1'], + }, + ], tableName: 'Role', }); }); @@ -174,8 +211,8 @@ describe('*** DataOperator: Handlers tests ***', () => { await DataOperator.handleIsolatedEntity({tableName: IsolatedEntities.SYSTEM, values}); expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({ - comparator: isRecordSystemEqualToRaw, - fieldName: 'name', + findMatchingRecordBy: isRecordSystemEqualToRaw, + fieldName: 'id', operator: operateSystemRecord, rawValues: values, tableName: 'System', @@ -190,10 +227,10 @@ describe('*** DataOperator: Handlers tests ***', () => { const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords'); - const values = [ + const values: RawTermsOfService[] = [ { id: 'tos-1', - acceptedAt: 1, + accepted_at: 1, create_at: 1613667352029, user_id: 'user1613667352029', text: '', @@ -206,8 +243,8 @@ describe('*** DataOperator: Handlers tests ***', () => { }); expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({ - comparator: isRecordTermsOfServiceEqualToRaw, - fieldName: 'accepted_at', + findMatchingRecordBy: isRecordTermsOfServiceEqualToRaw, + fieldName: 'id', operator: operateTermsOfServiceRecord, rawValues: values, tableName: 'TermsOfService', @@ -227,7 +264,7 @@ describe('*** DataOperator: Handlers tests ***', () => { tableName: 'INVALID_TABLE_NAME', // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - values: [{id: 'tos-1', acceptedAt: 1}], + values: [{id: 'tos-1', accepted_at: 1}], }), ).rejects.toThrow(DataOperatorException); }); @@ -297,7 +334,7 @@ describe('*** DataOperator: Handlers tests ***', () => { await DataOperator.handleDraft(values); expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({ - comparator: isRecordDraftEqualToRaw, + findMatchingRecordBy: isRecordDraftEqualToRaw, fieldName: 'channel_id', operator: operateDraftRecord, rawValues: values, @@ -630,7 +667,7 @@ describe('*** DataOperator: Handlers tests ***', () => { }); it('=> HandleUsers: should write to User entity', async () => { - expect.assertions(1); + expect.assertions(2); const users = [ { @@ -659,7 +696,7 @@ describe('*** DataOperator: Handlers tests ***', () => { channel: true, auto_responder_active: false, auto_responder_message: - 'Hello, I am out of office and unable to respond to messages.', + 'Hello, I am out of office and unable to respond to messages.', comments: 'never', desktop_notification_sound: 'Hello', push_status: 'online', @@ -675,17 +712,69 @@ describe('*** DataOperator: Handlers tests ***', () => { }, ]; - const spyOnExecuteInDatabase = jest.spyOn(DataOperator as any, 'executeInDatabase'); + const spyOnExecuteInDatabase = jest.spyOn( + DataOperator as any, + 'executeInDatabase', + ); await createConnection(true); await DataOperator.handleUsers(users); expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + id: '9ciscaqbrpd6d8s68k76xb9bte', + create_at: 1599457495881, + update_at: 1607683720173, + delete_at: 0, + username: 'a.l', + auth_service: 'saml', + email: 'a.l@mattermost.com', + email_verified: true, + is_bot: false, + nickname: '', + first_name: 'A', + last_name: 'L', + position: 'Mobile Engineer', + roles: 'system_user', + props: {}, + notify_props: { + desktop: 'all', + desktop_sound: true, + email: true, + first_name: true, + mention_keys: '', + push: 'mention', + channel: true, + auto_responder_active: false, + auto_responder_message: + 'Hello, I am out of office and unable to respond to messages.', + comments: 'never', + desktop_notification_sound: 'Hello', + push_status: 'online', + }, + last_password_update: 1604323112537, + last_picture_update: 1604686302260, + locale: 'en', + timezone: { + automaticTimezone: 'Indian/Mauritius', + manualTimezone: '', + useAutomaticTimezone: true, + }, + }, + }, + ], + tableName: 'User', + updateRaws: [], + recordOperator: operateUserRecord, + }); }); it('=> HandlePreferences: should write to PREFERENCE entity', async () => { - expect.assertions(1); + expect.assertions(2); const preferences = [ { @@ -722,10 +811,50 @@ describe('*** DataOperator: Handlers tests ***', () => { await DataOperator.handlePreferences(preferences); expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + user_id: '9ciscaqbrpd6d8s68k76xb9bte', + category: 'group_channel_show', + name: 'qj91hepgjfn6xr4acm5xzd8zoc', + value: 'true', + }, + }, + { + raw: { + user_id: '9ciscaqbrpd6d8s68k76xb9bte', + category: 'notifications', + name: 'email_interval', + value: '30', + }, + }, + { + raw: { + user_id: '9ciscaqbrpd6d8s68k76xb9bte', + category: 'theme', + name: '', + value: + '{"awayIndicator":"#c1b966","buttonBg":"#4cbba4","buttonColor":"#ffffff","centerChannelBg":"#2f3e4e","centerChannelColor":"#dddddd","codeTheme":"solarized-dark","dndIndicator":"#e81023","errorTextColor":"#ff6461","image":"/static/files/0b8d56c39baf992e5e4c58d74fde0fd6.png","linkColor":"#a4ffeb","mentionBg":"#b74a4a","mentionColor":"#ffffff","mentionHighlightBg":"#984063","mentionHighlightLink":"#a4ffeb","newMessageSeparator":"#5de5da","onlineIndicator":"#65dcc8","sidebarBg":"#1b2c3e","sidebarHeaderBg":"#1b2c3e","sidebarHeaderTextColor":"#ffffff","sidebarText":"#ffffff","sidebarTextActiveBorder":"#66b9a7","sidebarTextActiveColor":"#ffffff","sidebarTextHoverBg":"#4a5664","sidebarUnreadText":"#ffffff","type":"Mattermost Dark"}', + }, + }, + { + raw: { + user_id: '9ciscaqbrpd6d8s68k76xb9bte', + category: 'tutorial_step', + name: '9ciscaqbrpd6d8s68k76xb9bte', + value: '2', + }, + }, + ], + tableName: 'Preference', + updateRaws: [], + recordOperator: operatePreferenceRecord, + }); }); it('=> HandleTeamMemberships: should write to TEAM_MEMBERSHIP entity', async () => { - expect.assertions(1); + expect.assertions(2); const teamMembership = [ { @@ -740,17 +869,39 @@ describe('*** DataOperator: Handlers tests ***', () => { }, ]; - const spyOnExecuteInDatabase = jest.spyOn(DataOperator as any, 'executeInDatabase'); + const spyOnExecuteInDatabase = jest.spyOn( + DataOperator as any, + 'executeInDatabase', + ); await createConnection(true); await DataOperator.handleTeamMemberships(teamMembership); expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + team_id: 'a', + user_id: 'ab', + roles: '3ngdqe1e7tfcbmam4qgnxp91bw', + delete_at: 0, + scheme_guest: false, + scheme_user: true, + scheme_admin: false, + explicit_roles: '', + }, + }, + ], + tableName: 'TeamMembership', + updateRaws: [], + recordOperator: operateTeamMembershipRecord, + }); }); it('=> HandleCustomEmojis: should write to CUSTOM_EMOJI entity', async () => { - expect.assertions(1); + expect.assertions(2); const emojis = [ { id: 'i', @@ -762,17 +913,40 @@ describe('*** DataOperator: Handlers tests ***', () => { }, ]; - const spyOnExecuteInDatabase = jest.spyOn(DataOperator as any, 'executeInDatabase'); + const spyOnExecuteInDatabase = jest.spyOn( + DataOperator as any, + 'executeInDatabase', + ); await createConnection(true); - await DataOperator.handleIsolatedEntity({tableName: IsolatedEntities.CUSTOM_EMOJI, values: emojis}); + await DataOperator.handleIsolatedEntity({ + tableName: IsolatedEntities.CUSTOM_EMOJI, + values: emojis, + }); expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + id: 'i', + create_at: 1580913641769, + update_at: 1580913641769, + delete_at: 0, + creator_id: '4cprpki7ri81mbx8efixcsb8jo', + name: 'boomI', + }, + }, + ], + tableName: 'CustomEmoji', + updateRaws: [], + recordOperator: operateCustomEmojiRecord, + }); }); it('=> HandleGroupMembership: should write to GROUP_MEMBERSHIP entity', async () => { - expect.assertions(1); + expect.assertions(2); const groupMemberships = [ { user_id: 'u4cprpki7ri81mbx8efixcsb8jo', @@ -787,10 +961,23 @@ describe('*** DataOperator: Handlers tests ***', () => { await DataOperator.handleGroupMembership(groupMemberships); expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + user_id: 'u4cprpki7ri81mbx8efixcsb8jo', + group_id: 'g4cprpki7ri81mbx8efixcsb8jo', + }, + }, + ], + tableName: 'GroupMembership', + updateRaws: [], + recordOperator: operateGroupMembershipRecord, + }); }); it('=> HandleChannelMembership: should write to CHANNEL_MEMBERSHIP entity', async () => { - expect.assertions(1); + expect.assertions(2); const channelMemberships = [ { channel_id: '17bfnb1uwb8epewp4q3x3rx9go', @@ -841,12 +1028,66 @@ describe('*** DataOperator: Handlers tests ***', () => { await DataOperator.handleChannelMembership(channelMemberships); expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + channel_id: '17bfnb1uwb8epewp4q3x3rx9go', + user_id: '9ciscaqbrpd6d8s68k76xb9bte', + roles: 'wqyby5r5pinxxdqhoaomtacdhc', + last_viewed_at: 1613667352029, + msg_count: 3864, + mention_count: 0, + notify_props: { + desktop: 'default', + email: 'default', + ignore_channel_mentions: 'default', + mark_unread: 'mention', + push: 'default', + }, + last_update_at: 1613667352029, + scheme_guest: false, + scheme_user: true, + scheme_admin: false, + explicit_roles: '', + }, + }, + { + raw: { + channel_id: '1yw6gxfr4bn1jbyp9nr7d53yew', + user_id: '9ciscaqbrpd6d8s68k76xb9bte', + roles: 'channel_user', + last_viewed_at: 1615300540549, + msg_count: 16, + mention_count: 0, + notify_props: { + desktop: 'default', + email: 'default', + ignore_channel_mentions: 'default', + mark_unread: 'all', + push: 'default', + }, + last_update_at: 1615300540549, + scheme_guest: false, + scheme_user: true, + scheme_admin: false, + explicit_roles: '', + }, + }, + ], + tableName: 'ChannelMembership', + updateRaws: [], + recordOperator: operateChannelMembershipRecord, + }); }); it('=> HandleGroup: should write to GROUP entity', async () => { - expect.assertions(1); + expect.assertions(2); - const spyOnExecuteInDatabase = jest.spyOn(DataOperator as any, 'executeInDatabase'); + const spyOnExecuteInDatabase = jest.spyOn( + DataOperator as any, + 'executeInDatabase', + ); await createConnection(true); @@ -866,10 +1107,31 @@ describe('*** DataOperator: Handlers tests ***', () => { ]); expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + id: 'id_groupdfjdlfkjdkfdsf', + name: 'mobile_team', + display_name: 'mobile team', + description: '', + source: '', + remote_id: '', + create_at: 0, + update_at: 0, + delete_at: 0, + has_syncables: true, + }, + }, + ], + tableName: 'Group', + updateRaws: [], + recordOperator: operateGroupRecord, + }); }); it('=> HandleGroupsInTeam: should write to GROUPS_IN_TEAM entity', async () => { - expect.assertions(1); + expect.assertions(2); const spyOnExecuteInDatabase = jest.spyOn(DataOperator as any, 'executeInDatabase'); @@ -889,10 +1151,29 @@ describe('*** DataOperator: Handlers tests ***', () => { ]); expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + team_id: 'team_899', + team_display_name: '', + team_type: '', + group_id: 'group_id89', + auto_add: true, + create_at: 0, + delete_at: 0, + update_at: 0, + }, + }, + ], + tableName: 'GroupsInTeam', + updateRaws: [], + recordOperator: operateGroupsInTeamRecord, + }); }); it('=> HandleGroupsInChannel: should write to GROUPS_IN_CHANNEL entity', async () => { - expect.assertions(1); + expect.assertions(2); const spyOnExecuteInDatabase = jest.spyOn(DataOperator as any, 'executeInDatabase'); @@ -915,5 +1196,421 @@ describe('*** DataOperator: Handlers tests ***', () => { ]); expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + auto_add: true, + channel_display_name: '', + channel_id: 'channelid', + channel_type: '', + create_at: 0, + delete_at: 0, + group_id: 'groupId', + team_display_name: '', + team_id: '', + team_type: '', + update_at: 0, + }, + }, + ], + tableName: 'GroupsInChannel', + updateRaws: [], + recordOperator: operateGroupsInChannelRecord, + }); + }); + + it('=> HandleTeam: should write to TEAM entity', async () => { + expect.assertions(2); + + const spyOnExecuteInDatabase = jest.spyOn(DataOperator as any, 'executeInDatabase'); + + await createConnection(true); + + await DataOperator.handleTeam([ + { + id: 'rcgiyftm7jyrxnmdfdfa1osd8zswby', + create_at: 1445538153952, + update_at: 1588876392150, + delete_at: 0, + display_name: 'Contributors', + name: 'core', + description: '', + email: '', + type: 'O', + company_name: '', + allowed_domains: '', + invite_id: 'codoy5s743rq5mk18i7u5dfdfksz7e', + allow_open_invite: true, + last_team_icon_update: 1525181587639, + scheme_id: 'hbwgrncq1pfcdkpotzidfdmarn95o', + group_constrained: null, + }, + ]); + + expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + id: 'rcgiyftm7jyrxnmdfdfa1osd8zswby', + create_at: 1445538153952, + update_at: 1588876392150, + delete_at: 0, + display_name: 'Contributors', + name: 'core', + description: '', + email: '', + type: 'O', + company_name: '', + allowed_domains: '', + invite_id: 'codoy5s743rq5mk18i7u5dfdfksz7e', + allow_open_invite: true, + last_team_icon_update: 1525181587639, + scheme_id: 'hbwgrncq1pfcdkpotzidfdmarn95o', + group_constrained: null, + }, + }, + ], + tableName: 'Team', + updateRaws: [], + recordOperator: operateTeamRecord, + }); + }); + + it('=> HandleTeamChannelHistory: should write to TEAM_CHANNEL_HISTORY entity', async () => { + expect.assertions(2); + + const spyOnExecuteInDatabase = jest.spyOn(DataOperator as any, 'executeInDatabase'); + + await createConnection(true); + + await DataOperator.handleTeamChannelHistory([ + { + team_id: 'a', + channel_ids: ['ca', 'cb'], + }, + ]); + + expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [{raw: {team_id: 'a', channel_ids: ['ca', 'cb']}}], + tableName: 'TeamChannelHistory', + updateRaws: [], + recordOperator: operateTeamChannelHistoryRecord, + }); + }); + + it('=> HandleTeamSearchHistory: should write to TEAM_SEARCH_HISTORY entity', async () => { + expect.assertions(2); + + const spyOnExecuteInDatabase = jest.spyOn(DataOperator as any, 'executeInDatabase'); + + await createConnection(true); + + await DataOperator.handleTeamSearchHistory([ + { + team_id: 'a', + term: 'termA', + display_term: 'termA', + created_at: 1445538153952, + }, + ]); + + expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + team_id: 'a', + term: 'termA', + display_term: 'termA', + created_at: 1445538153952, + }, + }, + ], + tableName: 'TeamSearchHistory', + updateRaws: [], + recordOperator: operateTeamSearchHistoryRecord, + }); + }); + + it('=> HandleSlashCommand: should write to SLASH_COMMAND entity', async () => { + expect.assertions(2); + + const spyOnExecuteInDatabase = jest.spyOn(DataOperator as any, 'executeInDatabase'); + + await createConnection(true); + + await DataOperator.handleSlashCommand([ + { + id: 'command_1', + auto_complete: true, + auto_complete_desc: 'mock_command', + auto_complete_hint: 'hint', + create_at: 1445538153952, + creator_id: 'creator_id', + delete_at: 1445538153952, + description: 'description', + display_name: 'display_name', + icon_url: 'display_name', + method: 'get', + team_id: 'teamA', + token: 'token', + trigger: 'trigger', + update_at: 1445538153953, + url: 'url', + username: 'userA', + }, + ]); + + expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + id: 'command_1', + auto_complete: true, + auto_complete_desc: 'mock_command', + auto_complete_hint: 'hint', + create_at: 1445538153952, + creator_id: 'creator_id', + delete_at: 1445538153952, + description: 'description', + display_name: 'display_name', + icon_url: 'display_name', + method: 'get', + team_id: 'teamA', + token: 'token', + trigger: 'trigger', + update_at: 1445538153953, + url: 'url', + username: 'userA', + }, + }, + ], + tableName: 'SlashCommand', + updateRaws: [], + recordOperator: operateSlashCommandRecord, + }); + }); + + it('=> HandleMyTeam: should write to MY_TEAM entity', async () => { + expect.assertions(2); + + const spyOnExecuteInDatabase = jest.spyOn(DataOperator as any, 'executeInDatabase'); + + await createConnection(true); + + await DataOperator.handleMyTeam([ + { + team_id: 'teamA', + roles: 'roleA, roleB, roleC', + is_unread: true, + mentions_count: 3, + }, + ]); + + expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + team_id: 'teamA', + roles: 'roleA, roleB, roleC', + is_unread: true, + mentions_count: 3, + }, + }, + ], + tableName: 'MyTeam', + updateRaws: [], + recordOperator: operateMyTeamRecord, + }); + }); + + it('=> HandleChannel: should write to CHANNEL entity', async () => { + expect.assertions(2); + + const spyOnExecuteInDatabase = jest.spyOn(DataOperator as any, 'executeInDatabase'); + + await createConnection(true); + + await DataOperator.handleChannel([ + { + id: 'kjlw9j1ttnxwig7tnqgebg7dtipno', + create_at: 1600185541285, + update_at: 1604401077256, + delete_at: 0, + team_id: '', + type: 'D', + display_name: '', + name: 'gh781zkzkhh357b4bejephjz5u8daw__9ciscaqbrpd6d8s68k76xb9bte', + header: '(https://mattermost', + purpose: '', + last_post_at: 1617311494451, + total_msg_count: 585, + extra_update_at: 0, + creator_id: '', + scheme_id: null, + props: null, + group_constrained: null, + shared: null, + }, + ]); + + expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + id: 'kjlw9j1ttnxwig7tnqgebg7dtipno', + create_at: 1600185541285, + update_at: 1604401077256, + delete_at: 0, + team_id: '', + type: 'D', + display_name: '', + name: + 'gh781zkzkhh357b4bejephjz5u8daw__9ciscaqbrpd6d8s68k76xb9bte', + header: '(https://mattermost', + purpose: '', + last_post_at: 1617311494451, + total_msg_count: 585, + extra_update_at: 0, + creator_id: '', + scheme_id: null, + props: null, + group_constrained: null, + shared: null, + }, + }, + ], + tableName: 'Channel', + updateRaws: [], + recordOperator: operateChannelRecord, + }); + }); + + it('=> HandleMyChannelSettings: should write to MY_CHANNEL_SETTINGS entity', async () => { + expect.assertions(2); + + const spyOnExecuteInDatabase = jest.spyOn(DataOperator as any, 'executeInDatabase'); + + await createConnection(true); + + await DataOperator.handleMyChannelSettings([ + { + channel_id: 'c', + notify_props: { + desktop: 'all', + desktop_sound: true, + email: true, + first_name: true, + mention_keys: '', + push: 'mention', + channel: true, + }, + }, + ]); + + expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + channel_id: 'c', + notify_props: { + desktop: 'all', + desktop_sound: true, + email: true, + first_name: true, + mention_keys: '', + push: 'mention', + channel: true, + }, + }, + }, + ], + tableName: 'MyChannelSettings', + updateRaws: [], + recordOperator: operateMyChannelSettingsRecord, + }); + }); + + it('=> HandleChannelInfo: should write to CHANNEL_INFO entity', async () => { + expect.assertions(2); + + const spyOnExecuteInDatabase = jest.spyOn(DataOperator as any, 'executeInDatabase'); + + await createConnection(true); + + await DataOperator.handleChannelInfo([ + { + channel_id: 'c', + guest_count: 10, + header: 'channel info header', + member_count: 10, + pinned_post_count: 3, + purpose: 'sample channel ', + }, + ]); + + expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + channel_id: 'c', + guest_count: 10, + header: 'channel info header', + member_count: 10, + pinned_post_count: 3, + purpose: 'sample channel ', + }, + }, + ], + tableName: 'ChannelInfo', + updateRaws: [], + recordOperator: operateChannelInfoRecord, + }); + }); + + it('=> HandleMyChannel: should write to MY_CHANNEL entity', async () => { + expect.assertions(2); + + const spyOnExecuteInDatabase = jest.spyOn(DataOperator as any, 'executeInDatabase'); + + await createConnection(true); + + await DataOperator.handleMyChannel([ + { + channel_id: 'c', + last_post_at: 1617311494451, + last_viewed_at: 1617311494451, + mentions_count: 3, + message_count: 10, + roles: 'guest', + }, + ]); + + expect(spyOnExecuteInDatabase).toHaveBeenCalledTimes(1); + expect(spyOnExecuteInDatabase).toHaveBeenCalledWith({ + createRaws: [ + { + raw: { + channel_id: 'c', + last_post_at: 1617311494451, + last_viewed_at: 1617311494451, + mentions_count: 3, + message_count: 10, + roles: 'guest', + }, + }, + ], + tableName: 'MyChannel', + updateRaws: [], + recordOperator: operateMyChannelRecord, + }); }); }); diff --git a/app/database/admin/data_operator/operators/index.ts b/app/database/admin/data_operator/operators/index.ts index 4c3ac2973e..75ef23c16d 100644 --- a/app/database/admin/data_operator/operators/index.ts +++ b/app/database/admin/data_operator/operators/index.ts @@ -5,13 +5,16 @@ import {Q} from '@nozbe/watermelondb'; import Model from '@nozbe/watermelondb/Model'; import {MM_TABLES} from '@constants/database'; -import {User} from '@database/server/models'; import App from '@typings/database/app'; +import Channel from '@typings/database/channel'; +import ChannelInfo from '@typings/database/channel_info'; import ChannelMembership from '@typings/database/channel_membership'; import CustomEmoji from '@typings/database/custom_emoji'; import { DataFactoryArgs, RawApp, + RawChannel, + RawChannelInfo, RawChannelMembership, RawCustomEmoji, RawDraft, @@ -21,6 +24,9 @@ import { RawGroupMembership, RawGroupsInChannel, RawGroupsInTeam, + RawMyChannel, + RawMyChannelSettings, + RawMyTeam, RawPost, RawPostMetadata, RawPostsInChannel, @@ -29,8 +35,12 @@ import { RawReaction, RawRole, RawServers, + RawSlashCommand, RawSystem, + RawTeam, + RawTeamChannelHistory, RawTeamMembership, + RawTeamSearchHistory, RawTermsOfService, RawUser, } from '@typings/database/database'; @@ -42,6 +52,9 @@ import Group from '@typings/database/group'; import GroupMembership from '@typings/database/group_membership'; import GroupsInChannel from '@typings/database/groups_in_channel'; import GroupsInTeam from '@typings/database/groups_in_team'; +import MyChannel from '@typings/database/my_channel'; +import MyChannelSettings from '@typings/database/my_channel_settings'; +import MyTeam from '@typings/database/my_team'; import Post from '@typings/database/post'; import PostMetadata from '@typings/database/post_metadata'; import PostsInChannel from '@typings/database/posts_in_channel'; @@ -50,20 +63,30 @@ import Preference from '@typings/database/preference'; import Reaction from '@typings/database/reaction'; import Role from '@typings/database/role'; import Servers from '@typings/database/servers'; +import SlashCommand from '@typings/database/slash_command'; import System from '@typings/database/system'; +import Team from '@typings/database/team'; +import TeamChannelHistory from '@typings/database/team_channel_history'; import TeamMembership from '@typings/database/team_membership'; +import TeamSearchHistory from '@typings/database/team_search_history'; import TermsOfService from '@typings/database/terms_of_service'; +import User from '@typings/database/user'; const {APP, GLOBAL, SERVERS} = MM_TABLES.DEFAULT; const { + CHANNEL, + CHANNEL_INFO, CHANNEL_MEMBERSHIP, CUSTOM_EMOJI, DRAFT, FILE, GROUP, - GROUPS_IN_TEAM, GROUPS_IN_CHANNEL, + GROUPS_IN_TEAM, GROUP_MEMBERSHIP, + MY_CHANNEL, + MY_CHANNEL_SETTINGS, + MY_TEAM, POST, POSTS_IN_CHANNEL, POSTS_IN_THREAD, @@ -71,14 +94,16 @@ const { PREFERENCE, REACTION, ROLE, + SLASH_COMMAND, SYSTEM, + TEAM, + TEAM_CHANNEL_HISTORY, TEAM_MEMBERSHIP, + TEAM_SEARCH_HISTORY, TERMS_OF_SERVICE, USER, } = MM_TABLES.SERVER; -// TODO : Include timezone_count and member_count when you have the information for the group section - /** * operateAppRecord: Prepares record of entity 'App' from the DEFAULT database for update or create actions. * @param {DataFactoryArgs} operator @@ -93,9 +118,9 @@ export const operateAppRecord = async ({action, database, value}: DataFactoryArg const generator = (app: App) => { app._raw.id = isCreateAction ? app.id : record.id; - app.buildNumber = raw?.buildNumber; - app.createdAt = raw?.createdAt; - app.versionNumber = raw?.versionNumber; + app.buildNumber = raw?.build_number; + app.createdAt = raw?.created_at; + app.versionNumber = raw?.version_number; }; return operateBaseRecord({ @@ -148,10 +173,10 @@ export const operateServersRecord = async ({action, database, value}: DataFactor const generator = (servers: Servers) => { servers._raw.id = isCreateAction ? servers.id : record.id; - servers.dbPath = raw?.dbPath; - servers.displayName = raw?.displayName; - servers.mentionCount = raw?.mentionCount; - servers.unreadCount = raw?.unreadCount; + servers.dbPath = raw?.db_path; + servers.displayName = raw?.display_name; + servers.mentionCount = raw?.mention_count; + servers.unreadCount = raw?.unread_count; servers.url = raw?.url; }; @@ -262,7 +287,7 @@ export const operateTermsOfServiceRecord = async ({action, database, value}: Dat // id of TOS comes from server response const generator = (tos: TermsOfService) => { tos._raw.id = isCreateAction ? (raw?.id ?? tos.id) : record?.id; - tos.acceptedAt = raw?.acceptedAt; + tos.acceptedAt = raw?.accepted_at; }; return operateBaseRecord({ @@ -688,6 +713,8 @@ export const operateGroupsInTeamRecord = async ({action, database, value}: DataF const record = value.record as GroupsInTeam; const isCreateAction = action === OperationType.CREATE; + // FIXME : should include memberCount and timezoneCount or will it be by update action? + const generator = (groupsInTeam: GroupsInTeam) => { groupsInTeam._raw.id = isCreateAction ? groupsInTeam.id : record?.id; groupsInTeam.teamId = raw.team_id; @@ -704,7 +731,7 @@ export const operateGroupsInTeamRecord = async ({action, database, value}: DataF }; /** - * operateGroupsInChannelRecord: Prepares record of entity 'GROUPS_IN_TEAM' from the SERVER database for update or create actions. + * operateGroupsInChannelRecord: Prepares record of entity 'GROUPS_IN_CHANNEL' from the SERVER database for update or create actions. * @param {DataFactory} operator * @param {Database} operator.database * @param {MatchExistingRecord} operator.value @@ -715,6 +742,7 @@ export const operateGroupsInChannelRecord = async ({action, database, value}: Da const record = value.record as GroupsInChannel; const isCreateAction = action === OperationType.CREATE; + // FIXME : should include memberCount and timezoneCount or will it be by update action? const generator = (groupsInChannel: GroupsInChannel) => { groupsInChannel._raw.id = isCreateAction ? groupsInChannel.id : record?.id; groupsInChannel.channelId = raw.channel_id; @@ -730,6 +758,284 @@ export const operateGroupsInChannelRecord = async ({action, database, value}: Da }); }; +/** + * operateTeamRecord: Prepares record of entity 'TEAM' from the SERVER database for update or create actions. + * @param {DataFactory} operator + * @param {Database} operator.database + * @param {MatchExistingRecord} operator.value + * @returns {Promise} + */ +export const operateTeamRecord = async ({action, database, value}: DataFactoryArgs) => { + const raw = value.raw as RawTeam; + const record = value.record as Team; + const isCreateAction = action === OperationType.CREATE; + + // id of team comes from server response + const generator = (team: Team) => { + team._raw.id = isCreateAction ? (raw?.id ?? team.id) : record?.id; + team.isAllowOpenInvite = raw.allow_open_invite; + team.description = raw.description; + team.displayName = raw.display_name; + team.name = raw.name; + team.updateAt = raw.update_at; + team.type = raw.type; + team.allowedDomains = raw.allowed_domains; + team.isGroupConstrained = Boolean(raw.group_constrained); + team.lastTeamIconUpdatedAt = raw.last_team_icon_update; + }; + + return operateBaseRecord({ + action, + database, + tableName: TEAM, + value, + generator, + }); +}; + +/** + * operateTeamChannelHistoryRecord: Prepares record of entity 'TEAM_CHANNEL_HISTORY' from the SERVER database for update or create actions. + * @param {DataFactory} operator + * @param {Database} operator.database + * @param {MatchExistingRecord} operator.value + * @returns {Promise} + */ +export const operateTeamChannelHistoryRecord = async ({action, database, value}: DataFactoryArgs) => { + const raw = value.raw as RawTeamChannelHistory; + const record = value.record as TeamChannelHistory; + const isCreateAction = action === OperationType.CREATE; + + const generator = (teamChannelHistory: TeamChannelHistory) => { + teamChannelHistory._raw.id = isCreateAction ? (teamChannelHistory.id) : record?.id; + teamChannelHistory.teamId = raw.team_id; + teamChannelHistory.channelIds = raw.channel_ids; + }; + + return operateBaseRecord({ + action, + database, + tableName: TEAM_CHANNEL_HISTORY, + value, + generator, + }); +}; + +/** + * operateTeamSearchHistoryRecord: Prepares record of entity 'TEAM_SEARCH_HISTORY' from the SERVER database for update or create actions. + * @param {DataFactory} operator + * @param {Database} operator.database + * @param {MatchExistingRecord} operator.value + * @returns {Promise} + */ +export const operateTeamSearchHistoryRecord = async ({action, database, value}: DataFactoryArgs) => { + const raw = value.raw as RawTeamSearchHistory; + const record = value.record as TeamSearchHistory; + const isCreateAction = action === OperationType.CREATE; + + const generator = (teamSearchHistory: TeamSearchHistory) => { + teamSearchHistory._raw.id = isCreateAction ? (teamSearchHistory.id) : record?.id; + teamSearchHistory.createdAt = raw.created_at; + teamSearchHistory.displayTerm = raw.display_term; + teamSearchHistory.term = raw.term; + teamSearchHistory.teamId = raw.team_id; + }; + + return operateBaseRecord({ + action, + database, + tableName: TEAM_SEARCH_HISTORY, + value, + generator, + }); +}; + +/** + * operateSlashCommandRecord: Prepares record of entity 'SLASH_COMMAND' from the SERVER database for update or create actions. + * @param {DataFactory} operator + * @param {Database} operator.database + * @param {MatchExistingRecord} operator.value + * @returns {Promise} + */ +export const operateSlashCommandRecord = async ({action, database, value}: DataFactoryArgs) => { + const raw = value.raw as RawSlashCommand; + const record = value.record as SlashCommand; + const isCreateAction = action === OperationType.CREATE; + + // id of team comes from server response + const generator = (slashCommand: SlashCommand) => { + slashCommand._raw.id = isCreateAction ? (raw?.id ?? slashCommand.id) : record?.id; + slashCommand.isAutoComplete = raw.auto_complete; + slashCommand.description = raw.description; + slashCommand.displayName = raw.display_name; + slashCommand.hint = raw.auto_complete_hint; + slashCommand.method = raw.method; + slashCommand.teamId = raw.team_id; + slashCommand.token = raw.token; + slashCommand.trigger = raw.trigger; + slashCommand.updateAt = raw.update_at; + }; + + return operateBaseRecord({ + action, + database, + tableName: SLASH_COMMAND, + value, + generator, + }); +}; + +/** + * operateMyTeamRecord: Prepares record of entity 'MY_TEAM' from the SERVER database for update or create actions. + * @param {DataFactory} operator + * @param {Database} operator.database + * @param {MatchExistingRecord} operator.value + * @returns {Promise} + */ +export const operateMyTeamRecord = async ({action, database, value}: DataFactoryArgs) => { + const raw = value.raw as RawMyTeam; + const record = value.record as MyTeam; + const isCreateAction = action === OperationType.CREATE; + + const generator = (myTeam: MyTeam) => { + myTeam._raw.id = isCreateAction ? myTeam.id : record?.id; + myTeam.teamId = raw.team_id; + myTeam.roles = raw.roles; + myTeam.isUnread = raw.is_unread; + myTeam.mentionsCount = raw.mentions_count; + }; + + return operateBaseRecord({ + action, + database, + tableName: MY_TEAM, + value, + generator, + }); +}; + +/** + * operateChannelRecord: Prepares record of entity 'CHANNEL' from the SERVER database for update or create actions. + * @param {DataFactory} operator + * @param {Database} operator.database + * @param {MatchExistingRecord} operator.value + * @returns {Promise} + */ +export const operateChannelRecord = async ({action, database, value}: DataFactoryArgs) => { + const raw = value.raw as RawChannel; + const record = value.record as Channel; + const isCreateAction = action === OperationType.CREATE; + + // id of team comes from server response + const generator = (channel: Channel) => { + channel._raw.id = isCreateAction ? (raw?.id ?? channel.id) : record?.id; + channel.createAt = raw.create_at; + channel.creatorId = raw.creator_id; + channel.deleteAt = raw.delete_at; + channel.displayName = raw.display_name; + channel.isGroupConstrained = Boolean(raw.group_constrained); + channel.name = raw.name; + channel.teamId = raw.team_id; + channel.type = raw.type; + }; + + return operateBaseRecord({ + action, + database, + tableName: CHANNEL, + value, + generator, + }); +}; + +/** + * operateMyChannelSettingsRecord: Prepares record of entity 'MY_CHANNEL_SETTINGS' from the SERVER database for update or create actions. + * @param {DataFactory} operator + * @param {Database} operator.database + * @param {MatchExistingRecord} operator.value + * @returns {Promise} + */ +export const operateMyChannelSettingsRecord = async ({action, database, value}: DataFactoryArgs) => { + const raw = value.raw as RawMyChannelSettings; + const record = value.record as MyChannelSettings; + const isCreateAction = action === OperationType.CREATE; + + const generator = (myChannelSetting: MyChannelSettings) => { + myChannelSetting._raw.id = isCreateAction ? myChannelSetting.id : record?.id; + myChannelSetting.channelId = raw.channel_id; + myChannelSetting.notifyProps = raw.notify_props; + }; + + return operateBaseRecord({ + action, + database, + tableName: MY_CHANNEL_SETTINGS, + value, + generator, + }); +}; + +/** + * operateChannelInfoRecord: Prepares record of entity 'CHANNEL_INFO' from the SERVER database for update or create actions. + * @param {DataFactory} operator + * @param {Database} operator.database + * @param {MatchExistingRecord} operator.value + * @returns {Promise} + */ +export const operateChannelInfoRecord = async ({action, database, value}: DataFactoryArgs) => { + const raw = value.raw as RawChannelInfo; + const record = value.record as ChannelInfo; + const isCreateAction = action === OperationType.CREATE; + + const generator = (channelInfo: ChannelInfo) => { + channelInfo._raw.id = isCreateAction ? channelInfo.id : record?.id; + channelInfo.channelId = raw.channel_id; + channelInfo.guestCount = raw.guest_count; + channelInfo.header = raw.header; + channelInfo.memberCount = raw.member_count; + channelInfo.pinned_post_count = raw.pinned_post_count; + channelInfo.purpose = raw.purpose; + }; + + return operateBaseRecord({ + action, + database, + tableName: CHANNEL_INFO, + value, + generator, + }); +}; + +/** + * operateMyChannelRecord: Prepares record of entity 'MY_CHANNEL' from the SERVER database for update or create actions. + * @param {DataFactory} operator + * @param {Database} operator.database + * @param {MatchExistingRecord} operator.value + * @returns {Promise} + */ +export const operateMyChannelRecord = async ({action, database, value}: DataFactoryArgs) => { + const raw = value.raw as RawMyChannel; + const record = value.record as MyChannel; + const isCreateAction = action === OperationType.CREATE; + + const generator = (myChannel: MyChannel) => { + myChannel._raw.id = isCreateAction ? myChannel.id : record?.id; + myChannel.channelId = raw.channel_id; + myChannel.roles = raw.roles; + myChannel.messageCount = raw.message_count; + myChannel.mentionsCount = raw.mentions_count; + myChannel.lastPostAt = raw.last_post_at; + myChannel.lastViewedAt = raw.last_viewed_at; + }; + + return operateBaseRecord({ + action, + database, + tableName: MY_CHANNEL, + value, + generator, + }); +}; + /** * operateBaseRecord: This is the last step for each operator and depending on the 'action', it will either prepare an * existing record for UPDATE or prepare a collection for CREATE diff --git a/app/database/admin/data_operator/operators/test.ts b/app/database/admin/data_operator/operators/test.ts index fcf6c79c1b..efcf557aeb 100644 --- a/app/database/admin/data_operator/operators/test.ts +++ b/app/database/admin/data_operator/operators/test.ts @@ -6,7 +6,9 @@ import {DatabaseType, OperationType} from '@typings/database/enums'; import { operateAppRecord, + operateChannelInfoRecord, operateChannelMembershipRecord, + operateChannelRecord, operateCustomEmojiRecord, operateDraftRecord, operateFileRecord, @@ -15,6 +17,9 @@ import { operateGroupRecord, operateGroupsInChannelRecord, operateGroupsInTeamRecord, + operateMyChannelRecord, + operateMyChannelSettingsRecord, + operateMyTeamRecord, operatePostInThreadRecord, operatePostMetadataRecord, operatePostRecord, @@ -23,8 +28,12 @@ import { operateReactionRecord, operateRoleRecord, operateServersRecord, + operateSlashCommandRecord, operateSystemRecord, + operateTeamChannelHistoryRecord, operateTeamMembershipRecord, + operateTeamRecord, + operateTeamSearchHistoryRecord, operateTermsOfServiceRecord, operateUserRecord, } from './index'; @@ -69,16 +78,15 @@ describe('*** DataOperator: Operators tests ***', () => { value: { record: undefined, raw: { - buildNumber: 'build-7', - createdAt: 1, - id: 'id-18', - versionNumber: 'v-1', + build_number: 'build-7', + created_at: 1, + version_number: 'v-1', }, }, }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('App'); + expect(preparedRecords!.collection.modelClass.name).toBe('App'); }); it('=> operateGlobalRecord: should return an array of type Global', async () => { @@ -92,12 +100,12 @@ describe('*** DataOperator: Operators tests ***', () => { database: database!, value: { record: undefined, - raw: {id: 'g-1', name: 'g-n1', value: 'g-v1'}, + raw: {name: 'g-n1', value: 'g-v1'}, }, }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('Global'); + expect(preparedRecords!.collection.modelClass.name).toBe('Global'); }); it('=> operateServersRecord: should return an array of type Servers', async () => { @@ -112,18 +120,17 @@ describe('*** DataOperator: Operators tests ***', () => { value: { record: undefined, raw: { - dbPath: 'mm-server', - displayName: 's-displayName', - id: 's-1', - mentionCount: 1, - unreadCount: 0, + db_path: 'mm-server', + display_name: 's-displayName', + mention_count: 1, + unread_count: 0, url: 'https://community.mattermost.com', }, }, }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('Servers'); + expect(preparedRecords!.collection.modelClass.name).toBe('Servers'); }); it('=> operateRoleRecord: should return an array of type Role', async () => { @@ -146,7 +153,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('Role'); + expect(preparedRecords!.collection.modelClass.name).toBe('Role'); }); it('=> operateSystemRecord: should return an array of type System', async () => { @@ -165,7 +172,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('System'); + expect(preparedRecords!.collection.modelClass.name).toBe('System'); }); it('=> operateTermsOfServiceRecord: should return an array of type TermsOfService', async () => { @@ -181,7 +188,7 @@ describe('*** DataOperator: Operators tests ***', () => { record: undefined, raw: { id: 'tos-1', - acceptedAt: 1, + accepted_at: 1, create_at: 1613667352029, user_id: 'user1613667352029', text: '', @@ -190,7 +197,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch( + expect(preparedRecords!.collection.modelClass.name).toBe( 'TermsOfService', ); }); @@ -232,7 +239,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('Post'); + expect(preparedRecords!.collection.modelClass.name).toBe('Post'); }); it('=> operatePostInThreadRecord: should return an array of type PostsInThread', async () => { @@ -256,7 +263,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch( + expect(preparedRecords!.collection.modelClass.name).toBe( 'PostsInThread', ); }); @@ -285,7 +292,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('Reaction'); + expect(preparedRecords!.collection.modelClass.name).toBe('Reaction'); }); it('=> operateFileRecord: should return an array of type File', async () => { @@ -314,7 +321,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('File'); + expect(preparedRecords!.collection.modelClass.name).toBe('File'); }); it('=> operatePostMetadataRecord: should return an array of type PostMetadata', async () => { @@ -338,7 +345,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('PostMetadata'); + expect(preparedRecords!.collection.modelClass.name).toBe('PostMetadata'); }); it('=> operateDraftRecord: should return an array of type Draft', async () => { @@ -363,7 +370,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('Draft'); + expect(preparedRecords!.collection.modelClass.name).toBe('Draft'); }); it('=> operatePostsInChannelRecord: should return an array of type PostsInChannel', async () => { @@ -387,7 +394,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch( + expect(preparedRecords!.collection.modelClass.name).toBe( 'PostsInChannel', ); }); @@ -446,7 +453,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('User'); + expect(preparedRecords!.collection.modelClass.name).toBe('User'); }); it('=> operatePreferenceRecord: should return an array of type Preference', async () => { @@ -465,10 +472,10 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('Preference'); + expect(preparedRecords!.collection.modelClass.name).toBe('Preference'); }); - it('=> operatePreferenceRecord: should return an array of type TEAM_MEMBERSHIP', async () => { + it('=> operateTeamMembershipRecord: should return an array of type TeamMembership', async () => { expect.assertions(3); const database = await createConnection(); @@ -493,7 +500,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('TeamMembership'); + expect(preparedRecords!.collection.modelClass.name).toBe('TeamMembership'); }); it('=> operateCustomEmojiRecord: should return an array of type CustomEmoji', async () => { @@ -519,7 +526,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('CustomEmoji'); + expect(preparedRecords!.collection.modelClass.name).toBe('CustomEmoji'); }); it('=> operateGroupMembershipRecord: should return an array of type GroupMembership', async () => { @@ -542,7 +549,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('GroupMembership'); + expect(preparedRecords!.collection.modelClass.name).toBe('GroupMembership'); }); it('=> operateChannelMembershipRecord: should return an array of type ChannelMembership', async () => { @@ -580,7 +587,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('ChannelMembership'); + expect(preparedRecords!.collection.modelClass.name).toBe('ChannelMembership'); }); it('=> operateGroupRecord: should return an array of type Group', async () => { @@ -610,7 +617,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('Group'); + expect(preparedRecords!.collection.modelClass.name).toBe('Group'); }); it('=> operateGroupsInTeamRecord: should return an array of type GroupsInTeam', async () => { @@ -638,7 +645,7 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('GroupsInTeam'); + expect(preparedRecords!.collection.modelClass.name).toBe('GroupsInTeam'); }); it('=> operateGroupsInChannelRecord: should return an array of type GroupsInChannel', async () => { @@ -669,6 +676,269 @@ describe('*** DataOperator: Operators tests ***', () => { }); expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toMatch('GroupsInChannel'); + expect(preparedRecords!.collection.modelClass.name).toBe('GroupsInChannel'); + }); + + it('=> operateTeamRecord: should return an array of type Team', async () => { + expect.assertions(3); + + const database = await createConnection(); + expect(database).toBeTruthy(); + + const preparedRecords = await operateTeamRecord({ + action: OperationType.CREATE, + database: database!, + value: { + record: undefined, + raw: { + id: 'rcgiyftm7jyrxnmdfdfa1osd8zswby', + create_at: 1445538153952, + update_at: 1588876392150, + delete_at: 0, + display_name: 'Contributors', + name: 'core', + description: '', + email: '', + type: 'O', + company_name: '', + allowed_domains: '', + invite_id: 'codoy5s743rq5mk18i7u5dfdfksz7e', + allow_open_invite: true, + last_team_icon_update: 1525181587639, + scheme_id: 'hbwgrncq1pfcdkpotzidfdmarn95o', + group_constrained: null, + }, + }, + }); + + expect(preparedRecords).toBeTruthy(); + expect(preparedRecords!.collection.modelClass.name).toBe('Team'); + }); + + it('=> operateTeamChannelHistoryRecord: should return an array of type Team', async () => { + expect.assertions(3); + + const database = await createConnection(); + expect(database).toBeTruthy(); + + const preparedRecords = await operateTeamChannelHistoryRecord({ + action: OperationType.CREATE, + database: database!, + value: { + record: undefined, + raw: { + team_id: 'a', + channel_ids: ['ca', 'cb'], + }, + }, + }); + + expect(preparedRecords).toBeTruthy(); + expect(preparedRecords!.collection.modelClass.name).toBe('TeamChannelHistory'); + }); + + it('=> operateTeamSearchHistoryRecord: should return an array of type TeamSearchHistory', async () => { + expect.assertions(3); + + const database = await createConnection(); + expect(database).toBeTruthy(); + + const preparedRecords = await operateTeamSearchHistoryRecord({ + action: OperationType.CREATE, + database: database!, + value: { + record: undefined, + raw: { + team_id: 'a', + term: 'termA', + display_term: 'termA', + created_at: 1445538153952, + }, + }, + }); + + expect(preparedRecords).toBeTruthy(); + expect(preparedRecords!.collection.modelClass.name).toBe('TeamSearchHistory'); + }); + + it('=> operateSlashCommandRecord: should return an array of type SlashCommand', async () => { + expect.assertions(3); + + const database = await createConnection(); + expect(database).toBeTruthy(); + + const preparedRecords = await operateSlashCommandRecord({ + action: OperationType.CREATE, + database: database!, + value: { + record: undefined, + raw: { + id: 'command_1', + auto_complete: true, + auto_complete_desc: 'mock_command', + auto_complete_hint: 'hint', + create_at: 1445538153952, + creator_id: 'creator_id', + delete_at: 1445538153952, + description: 'description', + display_name: 'display_name', + icon_url: 'display_name', + method: 'get', + team_id: 'teamA', + token: 'token', + trigger: 'trigger', + update_at: 1445538153953, + url: 'url', + username: 'userA', + }, + }, + }); + + expect(preparedRecords).toBeTruthy(); + expect(preparedRecords!.collection.modelClass.name).toBe('SlashCommand'); + }); + + it('=> operateMyTeamRecord: should return an array of type MyTeam', async () => { + expect.assertions(3); + + const database = await createConnection(); + expect(database).toBeTruthy(); + + const preparedRecords = await operateMyTeamRecord({ + action: OperationType.CREATE, + database: database!, + value: { + record: undefined, + raw: { + team_id: 'teamA', + roles: 'roleA, roleB, roleC', + is_unread: true, + mentions_count: 3, + }, + }, + }); + + expect(preparedRecords).toBeTruthy(); + expect(preparedRecords!.collection.modelClass.name).toBe('MyTeam'); + }); + + it('=> operateChannelRecord: should return an array of type Channel', async () => { + expect.assertions(3); + + const database = await createConnection(); + expect(database).toBeTruthy(); + + const preparedRecords = await operateChannelRecord({ + action: OperationType.CREATE, + database: database!, + value: { + record: undefined, + raw: { + id: 'kow9j1ttnxwig7tnqgebg7dtipno', + create_at: 1600185541285, + update_at: 1604401077256, + delete_at: 0, + team_id: '', + type: 'D', + display_name: '', + name: 'jui1zkzkhh357b4bejephjz5u8daw__9ciscaqbrpd6d8s68k76xb9bte', + header: 'https://mattermost)', + purpose: '', + last_post_at: 1617311494451, + total_msg_count: 585, + extra_update_at: 0, + creator_id: '', + scheme_id: null, + props: null, + group_constrained: null, + shared: null, + }, + }, + }); + + expect(preparedRecords).toBeTruthy(); + expect(preparedRecords!.collection.modelClass.name).toBe('Channel'); + }); + + it('=> operateMyChannelSettingsRecord: should return an array of type MyChannelSettings', async () => { + expect.assertions(3); + + const database = await createConnection(); + expect(database).toBeTruthy(); + + const preparedRecords = await operateMyChannelSettingsRecord({ + action: OperationType.CREATE, + database: database!, + value: { + record: undefined, + raw: { + channel_id: 'c', + notify_props: { + desktop: 'all', + desktop_sound: true, + email: true, + first_name: true, + mention_keys: '', + push: 'mention', + channel: true, + }, + }, + }, + }); + + expect(preparedRecords).toBeTruthy(); + expect(preparedRecords!.collection.modelClass.name).toBe('MyChannelSettings'); + }); + + it('=> operateChannelInfoRecord: should return an array of type ChannelInfo', async () => { + expect.assertions(3); + + const database = await createConnection(); + expect(database).toBeTruthy(); + + const preparedRecords = await operateChannelInfoRecord({ + action: OperationType.CREATE, + database: database!, + value: { + record: undefined, + raw: { + channel_id: 'c', + guest_count: 10, + header: 'channel info header', + member_count: 10, + pinned_post_count: 3, + purpose: 'sample channel ', + }, + }, + }); + + expect(preparedRecords).toBeTruthy(); + expect(preparedRecords!.collection.modelClass.name).toBe('ChannelInfo'); + }); + + it('=> operateMyChannelRecord: should return an array of type MyChannel', async () => { + expect.assertions(3); + + const database = await createConnection(); + expect(database).toBeTruthy(); + + const preparedRecords = await operateMyChannelRecord({ + action: OperationType.CREATE, + database: database!, + value: { + record: undefined, + raw: { + channel_id: 'cd', + last_post_at: 1617311494451, + last_viewed_at: 1617311494451, + mentions_count: 3, + message_count: 10, + roles: 'guest', + }, + }, + }); + + expect(preparedRecords).toBeTruthy(); + expect(preparedRecords!.collection.modelClass.name).toBe('MyChannel'); }); }); diff --git a/app/database/admin/data_operator/utils/index.ts b/app/database/admin/data_operator/utils/index.ts index 8e2cebe9e2..3429420554 100644 --- a/app/database/admin/data_operator/utils/index.ts +++ b/app/database/admin/data_operator/utils/index.ts @@ -5,14 +5,19 @@ import {Q} from '@nozbe/watermelondb'; import Model from '@nozbe/watermelondb/Model'; import {MM_TABLES} from '@constants/database'; +import Channel from '@typings/database/channel'; import { ChainPostsArgs, IdenticalRecordArgs, MatchExistingRecord, RangeOfValueArgs, + RawChannel, RawPost, RawReaction, - RawUser, RawValue, + RawSlashCommand, + RawTeam, + RawUser, + RawValue, RecordPair, RetrieveRecordsArgs, SanitizePostsArgs, @@ -20,9 +25,11 @@ import { } from '@typings/database/database'; import Reaction from '@typings/database/reaction'; import Post from '@typings/database/post'; -import {User} from '@database/server/models'; +import SlashCommand from '@typings/database/slash_command'; +import Team from '@typings/database/team'; +import User from '@typings/database/user'; -const {POST, USER, REACTION} = MM_TABLES.SERVER; +const {CHANNEL, POST, REACTION, SLASH_COMMAND, TEAM, USER} = MM_TABLES.SERVER; /** * sanitizePosts: Creates arrays of ordered and unordered posts. Unordered posts are those posts that are not @@ -161,11 +168,11 @@ export const retrieveRecords = async ({database, tableName, condition}: Retrieve * @returns {boolean} */ export const hasSimilarUpdateAt = ({tableName, newValue, existingRecord}: IdenticalRecordArgs) => { - const guardTables = [POST, USER]; + const guardTables = [CHANNEL, POST, SLASH_COMMAND, TEAM, USER]; if (guardTables.includes(tableName)) { - type Raw = RawPost | RawUser - type ExistingRecord = Post | User + type Raw = RawPost | RawUser | RawTeam | RawSlashCommand | RawChannel + type ExistingRecord = Post | User | Team | SlashCommand | Channel return (newValue as Raw).update_at === (existingRecord as ExistingRecord).updateAt; } @@ -200,3 +207,18 @@ export const getRawRecordPairs = (raws: any[]): RecordPair[] => { return {raw, record: undefined}; }); }; + +/** + * getUniqueRawsBy: We have to ensure that we are not updating the same record twice in the same operation. + * Hence, thought it might not occur, prevention is better than cure. This function removes duplicates from the 'raws' array. + * @param {RawValue[]} raws + * @param {string} key + */ +export const getUniqueRawsBy = ({raws, key}:{ raws: RawValue[], key: string}) => { + return [...new Map(raws.map((item) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const curItemKey = item[key]; + return [curItemKey, item]; + })).values()]; +}; diff --git a/app/database/admin/database_manager/test.ts b/app/database/admin/database_manager/test.ts index 875abe7543..ff9f585942 100644 --- a/app/database/admin/database_manager/test.ts +++ b/app/database/admin/database_manager/test.ts @@ -167,7 +167,7 @@ describe('*** Database Manager tests ***', () => { const occurrences = allServers?.map((server) => server.url).reduce((acc, cur) => (cur === serverUrl ? acc + 1 : acc), 0); - // We should only have one occurence of the 'https://appv3.mattermost.com' url + // We should only have one occurrence of the 'https://appv3.mattermost.com' url expect(occurrences).toEqual(1); }); }); diff --git a/app/database/default/models/servers.ts b/app/database/default/models/servers.ts index ecfba43625..bcac8b1ddb 100644 --- a/app/database/default/models/servers.ts +++ b/app/database/default/models/servers.ts @@ -10,7 +10,7 @@ const {SERVERS} = MM_TABLES.DEFAULT; /** * The Server model will help us to identify the various servers a user will log in; in the context of - * multi-server support system. The dbPath field will hold the App-Groups file-path + * multi-server support system. The db_path field will hold the App-Groups file-path */ export default class Servers extends Model { /** table (entity name) : servers */ diff --git a/app/database/server/models/channel.ts b/app/database/server/models/channel.ts index ff4349e25c..f7511fc6de 100644 --- a/app/database/server/models/channel.ts +++ b/app/database/server/models/channel.ts @@ -78,6 +78,9 @@ export default class Channel extends Model { /** creator_id : The user who created this channel */ @field('creator_id') creatorId!: string; + /** update_at : The timestamp to when this channel was last updated on the server */ + @field('update_at') updateAt!: number; + /** delete_at : The deletion/archived date of this channel */ @field('delete_at') deleteAt!: number; diff --git a/app/database/server/models/my_team.ts b/app/database/server/models/my_team.ts index 376704572e..332ec0d256 100644 --- a/app/database/server/models/my_team.ts +++ b/app/database/server/models/my_team.ts @@ -30,7 +30,7 @@ export default class MyTeam extends Model { /** mentions_count : Count of posts in which the user has been mentioned */ @field('mentions_count') mentionsCount!: number; - /** roles : The different permissions that this user has in the team */ + /** roles : The different permissions that this user has in the team, concatenated together with comma to form a single string. */ @field('roles') roles!: string; /** team_id : The foreign key of the 'parent' Team entity */ diff --git a/app/database/server/models/slash_command.ts b/app/database/server/models/slash_command.ts index 4b3ef455d5..fca5184c14 100644 --- a/app/database/server/models/slash_command.ts +++ b/app/database/server/models/slash_command.ts @@ -48,6 +48,9 @@ export default class SlashCommand extends Model { /** trigger : A pattern/text used to recognize when a slash command needs to launch */ @field('trigger') trigger!: string; + /** update_at : The timestamp to when this command was last updated on the server */ + @field('update_at') updateAt!: number; + /** team : The related parent TEAM record */ @immutableRelation(TEAM, 'team_id') team!: Relation; } diff --git a/app/database/server/models/team.ts b/app/database/server/models/team.ts index 7fa400c94a..6e35a04aed 100644 --- a/app/database/server/models/team.ts +++ b/app/database/server/models/team.ts @@ -75,6 +75,9 @@ export default class Team extends Model { /** name : The name for the team */ @field('name') name!: string; + /** update_at : The timestamp to when this team was last updated on the server */ + @field('update_at') updateAt!: number; + /** type : The type of team ( e.g. open/private ) */ @field('type') type!: string; diff --git a/app/database/server/schema/table_schemas/channel.ts b/app/database/server/schema/table_schemas/channel.ts index 2b774b7f01..a12266c04c 100644 --- a/app/database/server/schema/table_schemas/channel.ts +++ b/app/database/server/schema/table_schemas/channel.ts @@ -18,5 +18,6 @@ export default tableSchema({ {name: 'name', type: 'string', isIndexed: true}, {name: 'team_id', type: 'string', isIndexed: true}, {name: 'type', type: 'string'}, + {name: 'update_at', type: 'number'}, ], }); diff --git a/app/database/server/schema/table_schemas/slash_command.ts b/app/database/server/schema/table_schemas/slash_command.ts index d4da717e35..b6b959727b 100644 --- a/app/database/server/schema/table_schemas/slash_command.ts +++ b/app/database/server/schema/table_schemas/slash_command.ts @@ -18,5 +18,6 @@ export default tableSchema({ {name: 'team_id', type: 'string', isIndexed: true}, {name: 'token', type: 'string'}, {name: 'trigger', type: 'string'}, + {name: 'update_at', type: 'number'}, ], }); diff --git a/app/database/server/schema/table_schemas/team.ts b/app/database/server/schema/table_schemas/team.ts index a08940658e..4f91031b62 100644 --- a/app/database/server/schema/table_schemas/team.ts +++ b/app/database/server/schema/table_schemas/team.ts @@ -18,5 +18,6 @@ export default tableSchema({ {name: 'last_team_icon_updated_at', type: 'number'}, {name: 'name', type: 'string'}, {name: 'type', type: 'string'}, + {name: 'update_at', type: 'number'}, ], }); diff --git a/app/database/server/schema/test.ts b/app/database/server/schema/test.ts index 52bbae2b00..09e37895b7 100644 --- a/app/database/server/schema/test.ts +++ b/app/database/server/schema/test.ts @@ -67,10 +67,15 @@ describe('*** Test schema for SERVER database ***', () => { creator_id: {name: 'creator_id', type: 'string', isIndexed: true}, delete_at: {name: 'delete_at', type: 'number'}, display_name: {name: 'display_name', type: 'string'}, - is_group_constrained: {name: 'is_group_constrained', type: 'boolean'}, + is_group_constrained: { + name: 'is_group_constrained', + type: 'boolean', + }, name: {name: 'name', type: 'string', isIndexed: true}, team_id: {name: 'team_id', type: 'string', isIndexed: true}, type: {name: 'type', type: 'string'}, + update_at: {name: 'update_at', type: 'number'}, + }, columnArray: [ {name: 'create_at', type: 'number'}, @@ -81,6 +86,7 @@ describe('*** Test schema for SERVER database ***', () => { {name: 'name', type: 'string', isIndexed: true}, {name: 'team_id', type: 'string', isIndexed: true}, {name: 'type', type: 'string'}, + {name: 'update_at', type: 'number'}, ], }, [CHANNEL_MEMBERSHIP]: { @@ -99,9 +105,7 @@ describe('*** Test schema for SERVER database ***', () => { columns: { name: {name: 'name', type: 'string'}, }, - columnArray: [ - {name: 'name', type: 'string'}, - ], + columnArray: [{name: 'name', type: 'string'}], }, [MY_CHANNEL]: { name: MY_CHANNEL, @@ -144,7 +148,6 @@ describe('*** Test schema for SERVER database ***', () => { {name: 'channel_id', type: 'string', isIndexed: true}, {name: 'earliest', type: 'number'}, {name: 'latest', type: 'number'}, - ], }, [DRAFT]: { @@ -367,6 +370,7 @@ describe('*** Test schema for SERVER database ***', () => { team_id: {name: 'team_id', type: 'string', isIndexed: true}, token: {name: 'token', type: 'string'}, trigger: {name: 'trigger', type: 'string'}, + update_at: {name: 'update_at', type: 'number'}, }, columnArray: [ {name: 'is_auto_complete', type: 'boolean'}, @@ -377,6 +381,7 @@ describe('*** Test schema for SERVER database ***', () => { {name: 'team_id', type: 'string', isIndexed: true}, {name: 'token', type: 'string'}, {name: 'trigger', type: 'string'}, + {name: 'update_at', type: 'number'}, ], }, [SYSTEM]: { @@ -393,14 +398,24 @@ describe('*** Test schema for SERVER database ***', () => { [TEAM]: { name: TEAM, columns: { - is_allow_open_invite: {name: 'is_allow_open_invite', type: 'boolean'}, + is_allow_open_invite: { + name: 'is_allow_open_invite', + type: 'boolean', + }, allowed_domains: {name: 'allowed_domains', type: 'string'}, description: {name: 'description', type: 'string'}, display_name: {name: 'display_name', type: 'string'}, - is_group_constrained: {name: 'is_group_constrained', type: 'boolean'}, - last_team_icon_updated_at: {name: 'last_team_icon_updated_at', type: 'number'}, + is_group_constrained: { + name: 'is_group_constrained', + type: 'boolean', + }, + last_team_icon_updated_at: { + name: 'last_team_icon_updated_at', + type: 'number', + }, name: {name: 'name', type: 'string'}, type: {name: 'type', type: 'string'}, + update_at: {name: 'update_at', type: 'number'}, }, columnArray: [ {name: 'is_allow_open_invite', type: 'boolean'}, @@ -411,6 +426,7 @@ describe('*** Test schema for SERVER database ***', () => { {name: 'last_team_icon_updated_at', type: 'number'}, {name: 'name', type: 'string'}, {name: 'type', type: 'string'}, + {name: 'update_at', type: 'number'}, ], }, [TEAM_CHANNEL_HISTORY]: { @@ -442,7 +458,6 @@ describe('*** Test schema for SERVER database ***', () => { display_term: {name: 'display_term', type: 'string'}, team_id: {name: 'team_id', type: 'string', isIndexed: true}, term: {name: 'term', type: 'string'}, - }, columnArray: [ {name: 'created_at', type: 'number'}, @@ -456,9 +471,7 @@ describe('*** Test schema for SERVER database ***', () => { columns: { accepted_at: {name: 'accepted_at', type: 'number'}, }, - columnArray: [ - {name: 'accepted_at', type: 'number'}, - ], + columnArray: [{name: 'accepted_at', type: 'number'}], }, [USER]: { name: USER, @@ -471,7 +484,10 @@ describe('*** Test schema for SERVER database ***', () => { is_bot: {name: 'is_bot', type: 'boolean'}, is_guest: {name: 'is_guest', type: 'boolean'}, last_name: {name: 'last_name', type: 'string'}, - last_picture_update: {name: 'last_picture_update', type: 'number'}, + last_picture_update: { + name: 'last_picture_update', + type: 'number', + }, locale: {name: 'locale', type: 'string'}, nickname: {name: 'nickname', type: 'string'}, notify_props: {name: 'notify_props', type: 'string'}, diff --git a/types/database/channel.d.ts b/types/database/channel.d.ts index da8d70028f..2c456a06d7 100644 --- a/types/database/channel.d.ts +++ b/types/database/channel.d.ts @@ -34,6 +34,9 @@ export default class Channel extends Model { /** delete_at : The deletion/archived date of this channel */ deleteAt: number; + /** update_at : The timestamp to when this channel was last updated on the server */ + updateAt!: number; + /** display_name : The channel display name (e.g. Town Square ) */ displayName: string; diff --git a/types/database/database.d.ts b/types/database/database.d.ts index 89f80c6492..696d39d9d1 100644 --- a/types/database/database.d.ts +++ b/types/database/database.d.ts @@ -31,24 +31,21 @@ export type DefaultNewServerArgs = { export type DatabaseInstance = Database | undefined; export type RawApp = { - buildNumber: string; - createdAt: number; - id: string; - versionNumber: string; + build_number: string; + created_at: number; + version_number: string; }; export type RawGlobal = { - id: string; name: string; value: string; }; export type RawServers = { - dbPath: string; - displayName: string; - id: string; - mentionCount: number; - unreadCount: number; + db_path: string; + display_name: string; + mention_count: number; + unread_count: number; url: string; }; @@ -78,7 +75,7 @@ export type RawSystem = { export type RawTermsOfService = { id: string; - acceptedAt: number; + accepted_at: number; create_at: number; user_id: string; text: string; @@ -191,8 +188,6 @@ export type RawPost = { }; }; -export type ChannelType = 'D' | 'O' | 'G' | 'P'; - export type RawUser = { id: string; auth_service: string; @@ -296,6 +291,110 @@ export type RawChannelMembers = { user_id: string; }; +export type RawPostsInThread = { + earliest: number; + latest?: number; + post_id: string; +}; + +export type RawGroup = { + create_at: number; + delete_at: number; + description: string; + display_name: string; + has_syncables: boolean; + id: string; + name: string; + remote_id: string; + source: string; + update_at: number; +}; + +export type RawGroupsInTeam = { + auto_add: boolean; + create_at: number; + delete_at: number; + group_id: string; + team_display_name: string; + team_id: string; + team_type: string; + update_at: number; +}; + +export type RawGroupsInChannel = { + auto_add: boolean; + channel_display_name: string; + channel_id: string; + channel_type: string; + create_at: number; + delete_at: number; + group_id: string; + team_display_name: string; + team_id: string; + team_type: string; + update_at: number; +}; + +export type RawTeam = { + id: string; + allow_open_invite: boolean; + allowed_domains: string; + company_name: string; + create_at: number; + delete_at: number; + description: string; + display_name: string; + email: string; + group_constrained: boolean | null; + invite_id: string; + last_team_icon_update: number; + name: string; + scheme_id: string; + type: string; + update_at: number; +}; + +export type RawTeamChannelHistory = { +team_id: string; +channel_ids: string[] +} + +export type RawTeamSearchHistory = { + created_at: number; + display_term: string; + term: string; + team_id: string; +} + +export type RawSlashCommand = { + id: string; + auto_complete: boolean; + auto_complete_desc: string; + auto_complete_hint: string; + create_at: number; + creator_id: string; + delete_at: number; + description: string; + display_name: string; + icon_url: string; + method: string; + team_id: string; + token: string; + trigger: string; + update_at: number; + url: string; + username: string; +}; + +export type RawMyTeam = { + team_id: string; + roles: string; + is_unread: boolean; + mentions_count: number; +}; + +export type ChannelType = 'D' | 'O' | 'G' | 'P'; + export type RawChannel = { create_at: number; creator_id: string; @@ -317,52 +416,33 @@ export type RawChannel = { update_at: number; }; -export type RawPostsInThread = { - earliest: number; - latest?: number; - post_id: string; -}; - -export type RawGroup = { - create_at: number, - delete_at: number, - description: string, - display_name: string, - has_syncables: boolean - id: string, - name: string, - remote_id: string, - source: string, - update_at: number, +export type RawMyChannelSettings = { + notify_props: NotifyProps, + channel_id: string; } -export type RawGroupsInTeam = { - auto_add: boolean, - create_at: number, - delete_at: number, - group_id: string, - team_display_name: string, - team_id: string, - team_type: string, - update_at: number +export type RawChannelInfo = { + channel_id: string; + guest_count: number; + header: string; + member_count: number; + pinned_post_count: number; + purpose: string; } -export type RawGroupsInChannel = { - auto_add: boolean, - channel_display_name: string, - channel_id: string, - channel_type: string, - create_at: number, - delete_at: number, - group_id: string, - team_display_name: string, - team_id: string, - team_type: string, - update_at: number +export type RawMyChannel = { + channel_id: string; + last_post_at: number; + last_viewed_at: number; + mentions_count: number; + message_count: number; + roles: string; } export type RawValue = | RawApp + | RawChannel + | RawChannelInfo | RawChannelMembership | RawCustomEmoji | RawDraft @@ -372,6 +452,9 @@ export type RawValue = | RawGroupMembership | RawGroupsInChannel | RawGroupsInTeam + | RawMyChannel + | RawMyChannelSettings + | RawMyTeam | RawPost | RawPostMetadata | RawPostsInChannel @@ -380,8 +463,12 @@ export type RawValue = | RawReaction | RawRole | RawServers + | RawSlashCommand | RawSystem + | RawTeam + | RawTeamChannelHistory | RawTeamMembership + | RawTeamSearchHistory | RawTermsOfService | RawUser; @@ -402,7 +489,9 @@ export type PrepareForDatabaseArgs = { recordOperator: (DataFactoryArgs) => void; }; -export type PrepareRecordsArgs = PrepareForDatabaseArgs & { database: Database; }; +export type PrepareRecordsArgs = PrepareForDatabaseArgs & { + database: Database; +}; export type BatchOperationsArgs = { database: Database; models: Model[] }; @@ -420,7 +509,10 @@ export type DatabaseConnectionArgs = { }; // The elements required to switch to another active server database -export type ActiveServerDatabaseArgs = { displayName: string; serverUrl: string }; +export type ActiveServerDatabaseArgs = { + displayName: string; + serverUrl: string; +}; export type HandleReactionsArgs = { prepareRowsOnly: boolean; @@ -477,11 +569,11 @@ export type ProcessInputsArgs = { rawValues: RawValue[]; tableName: string; fieldName: string; - comparator: (existing: Model, newElement: RawValue) => boolean; + findMatchingRecordBy: (existing: Model, newElement: RawValue) => boolean; }; export type HandleEntityRecordsArgs = { - comparator: (existing: Model, newElement: RawValue) => boolean; + findMatchingRecordBy: (existing: Model, newElement: RawValue) => boolean; fieldName: string; operator: (DataFactoryArgs) => Promise; rawValues: RawValue[]; @@ -501,4 +593,4 @@ export type RangeOfValueArgs = { export type RecordPair = { record?: Model; raw: RawValue; -} +}; diff --git a/types/database/my_team.d.ts b/types/database/my_team.d.ts index eb77583333..f0b6790c75 100644 --- a/types/database/my_team.d.ts +++ b/types/database/my_team.d.ts @@ -22,7 +22,7 @@ export default class MyTeam extends Model { /** mentions_count : Count of posts in which the user has been mentioned */ mentionsCount: number; - /** roles : The different permissions that this user has in the team */ + /** roles : The different permissions that this user has in the team, concatenated together with comma to form a single string. */ roles: string; /** team_id : The foreign key of the 'parent' Team entity */ diff --git a/types/database/servers.d.ts b/types/database/servers.d.ts index f6a7f7ed1b..aadb407767 100644 --- a/types/database/servers.d.ts +++ b/types/database/servers.d.ts @@ -5,7 +5,7 @@ import {Model} from '@nozbe/watermelondb'; /** * The Server model will help us to identify the various servers a user will log in; in the context of - * multi-server support system. The dbPath field will hold the App-Groups file-path + * multi-server support system. The db_path field will hold the App-Groups file-path */ export default class Servers extends Model { /** table (entity name) : servers */ diff --git a/types/database/slash_command.d.ts b/types/database/slash_command.d.ts index 3e34ddf299..3d5b94fdbc 100644 --- a/types/database/slash_command.d.ts +++ b/types/database/slash_command.d.ts @@ -40,6 +40,9 @@ export default class SlashCommand extends Model { /** trigger : A pattern/text used to recognize when a slash command needs to launch */ trigger: string; + /** update_at : The timestamp to when this command was last updated on the server */ + updateAt!: number; + /** team : The related parent TEAM record */ team: Relation; } diff --git a/types/database/team.d.ts b/types/database/team.d.ts index 9f1f455b46..4619763f21 100644 --- a/types/database/team.d.ts +++ b/types/database/team.d.ts @@ -31,6 +31,9 @@ export default class Team extends Model { /** display_name : The display name for the team */ displayName: string; + /** update_at : The timestamp to when this team was last updated on the server */ + updateAt!: number; + /** is_group_constrained : Boolean flag indicating if members are managed groups */ isGroupConstrained: boolean;