From 162bc6cc3fb5e40c11a88d95287e5e23b7c04ef2 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Thu, 3 Mar 2022 12:11:47 -0300 Subject: [PATCH] [Gekidou] various fixes (#6022) * remove empty roles before fetching * Fix prepare delete teams, channels and posts so they don't throw * Fix reaction operator and moved some handlers to their correct file * include delete categories when teams or channels are deleted * Remove unused array in fetchRoles * fix param comment for reactions handler * don't sync preferences when getting the WS event --- app/actions/remote/role.ts | 3 +- app/actions/websocket/preferences.ts | 2 - app/database/models/server/channel.ts | 4 + app/database/models/server/team.ts | 8 ++ .../handlers/channel.test.ts | 63 ++++++++++ .../server_data_operator/handlers/channel.ts | 39 ++++++- .../handlers/reaction.test.ts | 42 +++++++ .../server_data_operator/handlers/reaction.ts | 78 +++++++++++++ .../handlers/user.test.ts | 63 ---------- .../server_data_operator/handlers/user.ts | 108 +----------------- .../operator/server_data_operator/index.ts | 4 +- .../transformers/channel.test.ts | 37 ++++++ .../transformers/channel.ts | 50 +++++++- .../transformers/user.test.ts | 37 ------ .../server_data_operator/transformers/user.ts | 29 ----- app/database/operator/utils/reaction.ts | 12 +- app/queries/servers/categories.ts | 16 ++- app/queries/servers/channel.ts | 18 +-- app/queries/servers/post.ts | 6 +- app/queries/servers/team.ts | 60 ++++++---- types/database/models/servers/channel.d.ts | 3 + types/database/models/servers/team.d.ts | 7 +- 22 files changed, 399 insertions(+), 290 deletions(-) create mode 100644 app/database/operator/server_data_operator/handlers/reaction.test.ts create mode 100644 app/database/operator/server_data_operator/handlers/reaction.ts diff --git a/app/actions/remote/role.ts b/app/actions/remote/role.ts index c981fdbdc6..e2b80977ce 100644 --- a/app/actions/remote/role.ts +++ b/app/actions/remote/role.ts @@ -61,11 +61,9 @@ export const fetchRoles = async (serverUrl: string, teamMembership?: TeamMembers if (teamMembership?.length) { const teamRoles: string[] = []; - const teamMembers: string[] = []; teamMembership?.forEach((tm) => { teamRoles.push(...tm.roles.split(' ')); - teamMembers.push(tm.team_id); }); teamRoles.forEach(rolesToFetch.add, rolesToFetch); @@ -78,6 +76,7 @@ export const fetchRoles = async (serverUrl: string, teamMembership?: TeamMembers } } + rolesToFetch.delete(''); if (rolesToFetch.size > 0) { return fetchRolesIfNeeded(serverUrl, Array.from(rolesToFetch), fetchOnly); } diff --git a/app/actions/websocket/preferences.ts b/app/actions/websocket/preferences.ts index 9d8034f89b..2226e128ce 100644 --- a/app/actions/websocket/preferences.ts +++ b/app/actions/websocket/preferences.ts @@ -17,7 +17,6 @@ export async function handlePreferenceChangedEvent(serverUrl: string, msg: WebSo operator.handlePreferences({ prepareRecordsOnly: false, preferences: [preference], - sync: true, }); } } catch (error) { @@ -38,7 +37,6 @@ export async function handlePreferencesChangedEvent(serverUrl: string, msg: WebS operator.handlePreferences({ prepareRecordsOnly: false, preferences, - sync: true, }); } } catch (error) { diff --git a/app/database/models/server/channel.ts b/app/database/models/server/channel.ts index b47b129726..b6d7ee3d1f 100644 --- a/app/database/models/server/channel.ts +++ b/app/database/models/server/channel.ts @@ -7,6 +7,7 @@ import Model, {Associations} from '@nozbe/watermelondb/Model'; import {MM_TABLES} from '@constants/database'; +import type CategoryChannelModel from '@typings/database/models/servers/category_channel'; import type ChannelInfoModel from '@typings/database/models/servers/channel_info'; import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership'; import type DraftModel from '@typings/database/models/servers/draft'; @@ -127,6 +128,9 @@ export default class ChannelModel extends Model { /** settings: User specific settings/preferences for this channel */ @immutableRelation(MY_CHANNEL_SETTINGS, 'id') settings!: Relation; + /** categoryChannel : Query returning the membership data for the current user if it belongs to this channel */ + @immutableRelation(CATEGORY_CHANNEL, 'channel_id') categoryChannel!: Relation; + toApi = (): Channel => { return { id: this.id, diff --git a/app/database/models/server/team.ts b/app/database/models/server/team.ts index 2c13bf64bd..e819adb1f2 100644 --- a/app/database/models/server/team.ts +++ b/app/database/models/server/team.ts @@ -7,6 +7,7 @@ import Model, {Associations} from '@nozbe/watermelondb/Model'; import {MM_TABLES} from '@constants/database'; +import type CategoryModel from '@typings/database/models/servers/category'; import type ChannelModel from '@typings/database/models/servers/channel'; import type MyTeamModel from '@typings/database/models/servers/my_team'; import type SlashCommandModel from '@typings/database/models/servers/slash_command'; @@ -15,6 +16,7 @@ import type TeamMembershipModel from '@typings/database/models/servers/team_memb import type TeamSearchHistoryModel from '@typings/database/models/servers/team_search_history'; const { + CATEGORY, CHANNEL, TEAM, MY_TEAM, @@ -34,6 +36,9 @@ export default class TeamModel extends Model { /** associations : Describes every relationship to this table. */ static associations: Associations = { + /** A TEAM has a 1:N relationship with CATEGORY. A TEAM can possess multiple categories */ + [CATEGORY]: {type: 'has_many', foreignKey: 'team_id'}, + /** A TEAM has a 1:N relationship with CHANNEL. A TEAM can possess multiple channels */ [CHANNEL]: {type: 'has_many', foreignKey: 'team_id'}, @@ -77,6 +82,9 @@ export default class TeamModel extends Model { /** allowed_domains : List of domains that can join this team */ @field('allowed_domains') allowedDomains!: string; + /** categories : All the categories associated with this team */ + @children(CATEGORY) categories!: CategoryModel[]; + /** channels : All the channels associated with this team */ @children(CHANNEL) channels!: ChannelModel[]; diff --git a/app/database/operator/server_data_operator/handlers/channel.test.ts b/app/database/operator/server_data_operator/handlers/channel.test.ts index 6afce25065..ca858457cb 100644 --- a/app/database/operator/server_data_operator/handlers/channel.test.ts +++ b/app/database/operator/server_data_operator/handlers/channel.test.ts @@ -5,11 +5,13 @@ import DatabaseManager from '@database/manager'; import { isRecordChannelEqualToRaw, isRecordChannelInfoEqualToRaw, + isRecordChannelMembershipEqualToRaw, isRecordMyChannelEqualToRaw, isRecordMyChannelSettingsEqualToRaw, } from '@database/operator/server_data_operator/comparators'; import { transformChannelInfoRecord, + transformChannelMembershipRecord, transformChannelRecord, transformMyChannelRecord, transformMyChannelSettingsRecord, @@ -198,4 +200,65 @@ describe('*** Operator: Channel Handlers tests ***', () => { transformer: transformMyChannelRecord, }); }); + + it('=> HandleChannelMembership: should write to the CHANNEL_MEMBERSHIP table', async () => { + expect.assertions(2); + const channelMemberships: ChannelMembership[] = [ + { + id: '17bfnb1uwb8epewp4q3x3rx9go-9ciscaqbrpd6d8s68k76xb9bte', + 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_user: true, + scheme_admin: false, + }, + { + id: '1yw6gxfr4bn1jbyp9nr7d53yew-9ciscaqbrpd6d8s68k76xb9bte', + 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_user: true, + scheme_admin: false, + }, + ]; + + const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords'); + + await operator.handleChannelMembership({ + channelMemberships, + prepareRecordsOnly: false, + }); + + expect(spyOnHandleRecords).toHaveBeenCalledTimes(1); + expect(spyOnHandleRecords).toHaveBeenCalledWith({ + fieldName: 'user_id', + createOrUpdateRawValues: channelMemberships, + tableName: 'ChannelMembership', + prepareRecordsOnly: false, + findMatchingRecordBy: isRecordChannelMembershipEqualToRaw, + transformer: transformChannelMembershipRecord, + }); + }); }); diff --git a/app/database/operator/server_data_operator/handlers/channel.ts b/app/database/operator/server_data_operator/handlers/channel.ts index 534a0b2cd7..7c005f9fa0 100644 --- a/app/database/operator/server_data_operator/handlers/channel.ts +++ b/app/database/operator/server_data_operator/handlers/channel.ts @@ -6,32 +6,37 @@ import DataOperatorException from '@database/exceptions/data_operator_exception' import { isRecordChannelEqualToRaw, isRecordChannelInfoEqualToRaw, + isRecordChannelMembershipEqualToRaw, isRecordMyChannelEqualToRaw, isRecordMyChannelSettingsEqualToRaw, } from '@database/operator/server_data_operator/comparators'; import { transformChannelInfoRecord, + transformChannelMembershipRecord, transformChannelRecord, transformMyChannelRecord, transformMyChannelSettingsRecord, } from '@database/operator/server_data_operator/transformers/channel'; import {getUniqueRawsBy} from '@database/operator/utils/general'; -import type {HandleChannelArgs, HandleChannelInfoArgs, HandleMyChannelArgs, HandleMyChannelSettingsArgs} from '@typings/database/database'; +import type {HandleChannelArgs, HandleChannelInfoArgs, HandleChannelMembershipArgs, HandleMyChannelArgs, HandleMyChannelSettingsArgs} from '@typings/database/database'; import type ChannelModel from '@typings/database/models/servers/channel'; import type ChannelInfoModel from '@typings/database/models/servers/channel_info'; +import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership'; import type MyChannelModel from '@typings/database/models/servers/my_channel'; import type MyChannelSettingsModel from '@typings/database/models/servers/my_channel_settings'; const { CHANNEL, CHANNEL_INFO, + CHANNEL_MEMBERSHIP, MY_CHANNEL, MY_CHANNEL_SETTINGS, } = MM_TABLES.SERVER; export interface ChannelHandlerMix { handleChannel: ({channels, prepareRecordsOnly}: HandleChannelArgs) => Promise; + handleChannelMembership: ({channelMemberships, prepareRecordsOnly}: HandleChannelMembershipArgs) => Promise; handleMyChannelSettings: ({settings, prepareRecordsOnly}: HandleMyChannelSettingsArgs) => Promise; handleChannelInfo: ({channelInfos, prepareRecordsOnly}: HandleChannelInfoArgs) => Promise; handleMyChannel: ({channels, myChannels, prepareRecordsOnly}: HandleMyChannelArgs) => Promise; @@ -160,6 +165,38 @@ const ChannelHandler = (superclass: any) => class extends superclass { tableName: MY_CHANNEL, }); }; + + /** + * handleChannelMembership: Handler responsible for the Create/Update operations occurring on the CHANNEL_MEMBERSHIP table from the 'Server' schema + * @param {HandleChannelMembershipArgs} channelMembershipsArgs + * @param {ChannelMembership[]} channelMembershipsArgs.channelMemberships + * @param {boolean} channelMembershipsArgs.prepareRecordsOnly + * @throws DataOperatorException + * @returns {Promise} + */ + handleChannelMembership = ({channelMemberships, prepareRecordsOnly = true}: HandleChannelMembershipArgs): Promise => { + if (!channelMemberships.length) { + throw new DataOperatorException( + 'An empty "channelMemberships" array has been passed to the handleChannelMembership method', + ); + } + + const memberships: ChannelMember[] = channelMemberships.map((m) => ({ + id: `${m.channel_id}-${m.user_id}`, + ...m, + })); + + const createOrUpdateRawValues = getUniqueRawsBy({raws: memberships, key: 'id'}); + + return this.handleRecords({ + fieldName: 'user_id', + findMatchingRecordBy: isRecordChannelMembershipEqualToRaw, + transformer: transformChannelMembershipRecord, + prepareRecordsOnly, + createOrUpdateRawValues, + tableName: CHANNEL_MEMBERSHIP, + }); + }; }; export default ChannelHandler; diff --git a/app/database/operator/server_data_operator/handlers/reaction.test.ts b/app/database/operator/server_data_operator/handlers/reaction.test.ts new file mode 100644 index 0000000000..c24e474b2b --- /dev/null +++ b/app/database/operator/server_data_operator/handlers/reaction.test.ts @@ -0,0 +1,42 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import DatabaseManager from '@database/manager'; +import ServerDataOperator from '@database/operator/server_data_operator'; + +describe('*** Operator: User Handlers tests ***', () => { + let operator: ServerDataOperator; + + beforeAll(async () => { + await DatabaseManager.init(['baseHandler.test.com']); + operator = DatabaseManager.serverDatabases['baseHandler.test.com'].operator; + }); + + it('=> HandleReactions: should write to Reactions table', async () => { + expect.assertions(2); + + const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords'); + const spyOnBatchOperation = jest.spyOn(operator, 'batchRecords'); + + await operator.handleReactions({ + postsReactions: [{ + post_id: '4r9jmr7eqt8dxq3f9woypzurry', + reactions: [ + { + create_at: 1608263728086, + emoji_name: 'p4p1', + post_id: '4r9jmr7eqt8dxq3f9woypzurry', + user_id: 'ooumoqgq3bfiijzwbn8badznwc', + }, + ], + }], + prepareRecordsOnly: false, + }); + + // Called twice: Once for Reaction record + expect(spyOnPrepareRecords).toHaveBeenCalledTimes(1); + + // Only one batch operation for both tables + expect(spyOnBatchOperation).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/database/operator/server_data_operator/handlers/reaction.ts b/app/database/operator/server_data_operator/handlers/reaction.ts new file mode 100644 index 0000000000..08c3c2e282 --- /dev/null +++ b/app/database/operator/server_data_operator/handlers/reaction.ts @@ -0,0 +1,78 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {MM_TABLES} from '@constants/database'; +import DataOperatorException from '@database/exceptions/data_operator_exception'; +import {transformReactionRecord} from '@database/operator/server_data_operator/transformers/user'; +import {sanitizeReactions} from '@database/operator/utils/reaction'; + +import type {HandleReactionsArgs} from '@typings/database/database'; +import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji'; +import type ReactionModel from '@typings/database/models/servers/reaction'; + +const {REACTION} = MM_TABLES.SERVER; + +export interface ReactionHandlerMix { + handleReactions: ({postsReactions, prepareRecordsOnly}: HandleReactionsArgs) => Promise>; +} + +const ReactionHandler = (superclass: any) => class extends superclass { + /** + * handleReactions: Handler responsible for the Create/Update operations occurring on the Reaction table from the 'Server' schema + * @param {HandleReactionsArgs} handleReactions + * @param {ReactionsPerPost[]} handleReactions.postsReactions + * @param {boolean} handleReactions.prepareRecordsOnly + * @param {boolean} handleReactions.skipSync + * @throws DataOperatorException + * @returns {Promise>} + */ + handleReactions = async ({postsReactions, prepareRecordsOnly, skipSync}: HandleReactionsArgs): Promise => { + const batchRecords: ReactionModel[] = []; + + if (!postsReactions.length) { + throw new DataOperatorException( + 'An empty "reactions" array has been passed to the handleReactions method', + ); + } + + for await (const postReactions of postsReactions) { + const {post_id, reactions} = postReactions; + const { + createReactions, + deleteReactions, + } = await sanitizeReactions({ + database: this.database, + post_id, + rawReactions: reactions, + skipSync, + }); + + if (createReactions?.length) { + // Prepares record for model Reactions + const reactionsRecords = (await this.prepareRecords({ + createRaws: createReactions, + transformer: transformReactionRecord, + tableName: REACTION, + })) as ReactionModel[]; + batchRecords.push(...reactionsRecords); + } + + if (deleteReactions?.length && !skipSync) { + deleteReactions.forEach((outCast) => outCast.prepareDestroyPermanently()); + batchRecords.push(...deleteReactions); + } + } + + if (prepareRecordsOnly) { + return batchRecords; + } + + if (batchRecords?.length) { + await this.batchRecords(batchRecords); + } + + return batchRecords; + }; +}; + +export default ReactionHandler; diff --git a/app/database/operator/server_data_operator/handlers/user.test.ts b/app/database/operator/server_data_operator/handlers/user.test.ts index a777890d6b..aab9a15598 100644 --- a/app/database/operator/server_data_operator/handlers/user.test.ts +++ b/app/database/operator/server_data_operator/handlers/user.test.ts @@ -4,12 +4,10 @@ import DatabaseManager from '@database/manager'; import ServerDataOperator from '@database/operator/server_data_operator'; import { - isRecordChannelMembershipEqualToRaw, isRecordPreferenceEqualToRaw, isRecordUserEqualToRaw, } from '@database/operator/server_data_operator/comparators'; import { - transformChannelMembershipRecord, transformPreferenceRecord, transformUserRecord, } from '@database/operator/server_data_operator/transformers/user'; @@ -157,65 +155,4 @@ describe('*** Operator: User Handlers tests ***', () => { transformer: transformPreferenceRecord, }); }); - - it('=> HandleChannelMembership: should write to the CHANNEL_MEMBERSHIP table', async () => { - expect.assertions(2); - const channelMemberships: ChannelMembership[] = [ - { - id: '17bfnb1uwb8epewp4q3x3rx9go-9ciscaqbrpd6d8s68k76xb9bte', - 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_user: true, - scheme_admin: false, - }, - { - id: '1yw6gxfr4bn1jbyp9nr7d53yew-9ciscaqbrpd6d8s68k76xb9bte', - 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_user: true, - scheme_admin: false, - }, - ]; - - const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords'); - - await operator.handleChannelMembership({ - channelMemberships, - prepareRecordsOnly: false, - }); - - expect(spyOnHandleRecords).toHaveBeenCalledTimes(1); - expect(spyOnHandleRecords).toHaveBeenCalledWith({ - fieldName: 'user_id', - createOrUpdateRawValues: channelMemberships, - tableName: 'ChannelMembership', - prepareRecordsOnly: false, - findMatchingRecordBy: isRecordChannelMembershipEqualToRaw, - transformer: transformChannelMembershipRecord, - }); - }); }); diff --git a/app/database/operator/server_data_operator/handlers/user.ts b/app/database/operator/server_data_operator/handlers/user.ts index 804ca97963..83040153b4 100644 --- a/app/database/operator/server_data_operator/handlers/user.ts +++ b/app/database/operator/server_data_operator/handlers/user.ts @@ -4,78 +4,30 @@ import {MM_TABLES} from '@constants/database'; import DataOperatorException from '@database/exceptions/data_operator_exception'; import { - isRecordChannelMembershipEqualToRaw, isRecordPreferenceEqualToRaw, isRecordUserEqualToRaw, } from '@database/operator/server_data_operator/comparators'; import { - transformChannelMembershipRecord, transformPreferenceRecord, - transformReactionRecord, transformUserRecord, } from '@database/operator/server_data_operator/transformers/user'; import {getUniqueRawsBy} from '@database/operator/utils/general'; -import {sanitizeReactions} from '@database/operator/utils/reaction'; import type { - HandleChannelMembershipArgs, HandlePreferencesArgs, - HandleReactionsArgs, HandleUsersArgs, } from '@typings/database/database'; -import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership'; -import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji'; import type PreferenceModel from '@typings/database/models/servers/preference'; -import type ReactionModel from '@typings/database/models/servers/reaction'; import type UserModel from '@typings/database/models/servers/user'; -const { - CHANNEL_MEMBERSHIP, - PREFERENCE, - REACTION, - USER, -} = MM_TABLES.SERVER; +const {PREFERENCE, USER} = MM_TABLES.SERVER; export interface UserHandlerMix { - handleChannelMembership: ({channelMemberships, prepareRecordsOnly}: HandleChannelMembershipArgs) => Promise; handlePreferences: ({preferences, prepareRecordsOnly}: HandlePreferencesArgs) => Promise; - handleReactions: ({postsReactions, prepareRecordsOnly}: HandleReactionsArgs) => Promise>; handleUsers: ({users, prepareRecordsOnly}: HandleUsersArgs) => Promise; } const UserHandler = (superclass: any) => class extends superclass { - /** - * handleChannelMembership: Handler responsible for the Create/Update operations occurring on the CHANNEL_MEMBERSHIP table from the 'Server' schema - * @param {HandleChannelMembershipArgs} channelMembershipsArgs - * @param {ChannelMembership[]} channelMembershipsArgs.channelMemberships - * @param {boolean} channelMembershipsArgs.prepareRecordsOnly - * @throws DataOperatorException - * @returns {Promise} - */ - handleChannelMembership = ({channelMemberships, prepareRecordsOnly = true}: HandleChannelMembershipArgs): Promise => { - if (!channelMemberships.length) { - throw new DataOperatorException( - 'An empty "channelMemberships" array has been passed to the handleChannelMembership method', - ); - } - - const memberships: ChannelMember[] = channelMemberships.map((m) => ({ - id: `${m.channel_id}-${m.user_id}`, - ...m, - })); - - const createOrUpdateRawValues = getUniqueRawsBy({raws: memberships, key: 'id'}); - - return this.handleRecords({ - fieldName: 'user_id', - findMatchingRecordBy: isRecordChannelMembershipEqualToRaw, - transformer: transformChannelMembershipRecord, - prepareRecordsOnly, - createOrUpdateRawValues, - tableName: CHANNEL_MEMBERSHIP, - }); - }; - /** * handlePreferences: Handler responsible for the Create/Update operations occurring on the PREFERENCE table from the 'Server' schema * @param {HandlePreferencesArgs} preferencesArgs @@ -124,64 +76,6 @@ const UserHandler = (superclass: any) => class extends superclass { return records; }; - /** - * handleReactions: Handler responsible for the Create/Update operations occurring on the Reaction table from the 'Server' schema - * @param {HandleReactionsArgs} handleReactions - * @param {ReactionsPerPost[]} handleReactions.reactions - * @param {boolean} handleReactions.prepareRecordsOnly - * @param {boolean} handleReactions.skipSync - * @throws DataOperatorException - * @returns {Promise>} - */ - handleReactions = async ({postsReactions, prepareRecordsOnly, skipSync}: HandleReactionsArgs): Promise => { - const batchRecords: ReactionModel[] = []; - - if (!postsReactions.length) { - throw new DataOperatorException( - 'An empty "reactions" array has been passed to the handleReactions method', - ); - } - - for await (const postReactions of postsReactions) { - const {post_id, reactions} = postReactions; - const rawValues = getUniqueRawsBy({raws: reactions, key: 'emoji_name'}) as Reaction[]; - const { - createReactions, - deleteReactions, - } = await sanitizeReactions({ - database: this.database, - post_id, - rawReactions: rawValues, - skipSync, - }); - - if (createReactions?.length) { - // Prepares record for model Reactions - const reactionsRecords = (await this.prepareRecords({ - createRaws: createReactions, - transformer: transformReactionRecord, - tableName: REACTION, - })) as ReactionModel[]; - batchRecords.push(...reactionsRecords); - } - - if (deleteReactions?.length && !skipSync) { - deleteReactions.forEach((outCast) => outCast.prepareDestroyPermanently()); - batchRecords.push(...deleteReactions); - } - } - - if (prepareRecordsOnly) { - return batchRecords; - } - - if (batchRecords?.length) { - await this.batchRecords(batchRecords); - } - - return batchRecords; - }; - /** * handleUsers: Handler responsible for the Create/Update operations occurring on the User table from the 'Server' schema * @param {HandleUsersArgs} usersArgs diff --git a/app/database/operator/server_data_operator/index.ts b/app/database/operator/server_data_operator/index.ts index 12c47d0a6d..0b60d43869 100644 --- a/app/database/operator/server_data_operator/index.ts +++ b/app/database/operator/server_data_operator/index.ts @@ -7,6 +7,7 @@ import ChannelHandler, {ChannelHandlerMix} from '@database/operator/server_data_ import PostHandler, {PostHandlerMix} from '@database/operator/server_data_operator/handlers/post'; import PostsInChannelHandler, {PostsInChannelHandlerMix} from '@database/operator/server_data_operator/handlers/posts_in_channel'; import PostsInThreadHandler, {PostsInThreadHandlerMix} from '@database/operator/server_data_operator/handlers/posts_in_thread'; +import ReactionHander, {ReactionHandlerMix} from '@database/operator/server_data_operator/handlers/reaction'; import TeamHandler, {TeamHandlerMix} from '@database/operator/server_data_operator/handlers/team'; import UserHandler, {UserHandlerMix} from '@database/operator/server_data_operator/handlers/user'; import mix from '@utils/mix'; @@ -14,7 +15,7 @@ import mix from '@utils/mix'; import type {Database} from '@nozbe/watermelondb'; interface ServerDataOperator extends ServerDataOperatorBase, PostHandlerMix, PostsInChannelHandlerMix, - PostsInThreadHandlerMix, UserHandlerMix, ChannelHandlerMix, CategoryHandlerMix, TeamHandlerMix {} + PostsInThreadHandlerMix, ReactionHandlerMix, UserHandlerMix, ChannelHandlerMix, CategoryHandlerMix, TeamHandlerMix {} class ServerDataOperator extends mix(ServerDataOperatorBase).with( CategoryHandler, @@ -22,6 +23,7 @@ class ServerDataOperator extends mix(ServerDataOperatorBase).with( PostHandler, PostsInChannelHandler, PostsInThreadHandler, + ReactionHander, TeamHandler, UserHandler, ) { diff --git a/app/database/operator/server_data_operator/transformers/channel.test.ts b/app/database/operator/server_data_operator/transformers/channel.test.ts index 0c8a4e663c..71cc8f64d2 100644 --- a/app/database/operator/server_data_operator/transformers/channel.test.ts +++ b/app/database/operator/server_data_operator/transformers/channel.test.ts @@ -6,6 +6,7 @@ import { transformChannelRecord, transformMyChannelRecord, transformMyChannelSettingsRecord, + transformChannelMembershipRecord, } from '@database/operator/server_data_operator/transformers/channel'; import {createTestConnection} from '@database/operator/utils/create_test_connection'; import {OperationType} from '@typings/database/enums'; @@ -139,4 +140,40 @@ describe('*** CHANNEL Prepare Records Test ***', () => { expect(preparedRecords).toBeTruthy(); expect(preparedRecords!.collection.modelClass.name).toBe('MyChannelModel'); }); + + it('=> transformChannelMembershipRecord: should return an array of type ChannelMembership', async () => { + expect.assertions(3); + + const database = await createTestConnection({databaseName: 'user_prepare_records', setActive: true}); + expect(database).toBeTruthy(); + + const preparedRecords = await transformChannelMembershipRecord({ + action: OperationType.CREATE, + database: database!, + value: { + record: undefined, + 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_user: true, + scheme_admin: false, + }, + }, + }); + + expect(preparedRecords).toBeTruthy(); + expect(preparedRecords!.collection.modelClass.name).toBe('ChannelMembershipModel'); + }); }); diff --git a/app/database/operator/server_data_operator/transformers/channel.ts b/app/database/operator/server_data_operator/transformers/channel.ts index b18ca2abef..bc4c4becd6 100644 --- a/app/database/operator/server_data_operator/transformers/channel.ts +++ b/app/database/operator/server_data_operator/transformers/channel.ts @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {General} from '@constants'; import {MM_TABLES} from '@constants/database'; import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index'; import {OperationType} from '@typings/database/enums'; @@ -8,12 +9,14 @@ import {OperationType} from '@typings/database/enums'; import type {TransformerArgs} from '@typings/database/database'; import type ChannelModel from '@typings/database/models/servers/channel'; import type ChannelInfoModel from '@typings/database/models/servers/channel_info'; +import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership'; import type MyChannelModel from '@typings/database/models/servers/my_channel'; import type MyChannelSettingsModel from '@typings/database/models/servers/my_channel_settings'; const { CHANNEL, CHANNEL_INFO, + CHANNEL_MEMBERSHIP, MY_CHANNEL, MY_CHANNEL_SETTINGS, } = MM_TABLES.SERVER; @@ -37,9 +40,25 @@ export const transformChannelRecord = ({action, database, value}: TransformerArg channel.creatorId = raw.creator_id; channel.deleteAt = raw.delete_at; - // for DM channels do not override the display name + // for DM & GM's channels do not override the display name // until we get the new info if there is any - channel.displayName = raw.display_name || record?.displayName || ''; + let displayName; + if (raw.type === General.DM_CHANNEL && record?.displayName) { + displayName = raw.display_name || record?.displayName; + } else if (raw.type === General.GM_CHANNEL) { + const rawMembers = raw.display_name.split(',').length; + const recordMembers = record?.displayName.split(',').length || rawMembers; + + if (recordMembers < rawMembers) { + displayName = record.displayName; + } else { + displayName = raw.display_name; + } + } else { + displayName = raw.display_name; + } + + channel.displayName = displayName; channel.isGroupConstrained = Boolean(raw.group_constrained); channel.name = raw.name; channel.shared = Boolean(raw.shared); @@ -144,3 +163,30 @@ export const transformMyChannelRecord = ({action, database, value}: TransformerA }) as Promise; }; +/** + * transformChannelMembershipRecord: Prepares a record of the SERVER database 'ChannelMembership' table for update or create actions. + * @param {TransformerArgs} operator + * @param {Database} operator.database + * @param {RecordPair} operator.value + * @returns {Promise} + */ +export const transformChannelMembershipRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw as ChannelMembership; + const record = value.record as ChannelMembershipModel; + const isCreateAction = action === OperationType.CREATE; + + // If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database + const fieldsMapper = (channelMember: ChannelMembershipModel) => { + channelMember._raw.id = isCreateAction ? (raw?.id ?? channelMember.id) : record.id; + channelMember.channelId = raw.channel_id; + channelMember.userId = raw.user_id; + }; + + return prepareBaseRecord({ + action, + database, + tableName: CHANNEL_MEMBERSHIP, + value, + fieldsMapper, + }) as Promise; +}; diff --git a/app/database/operator/server_data_operator/transformers/user.test.ts b/app/database/operator/server_data_operator/transformers/user.test.ts index 5a84553871..0e214a2328 100644 --- a/app/database/operator/server_data_operator/transformers/user.test.ts +++ b/app/database/operator/server_data_operator/transformers/user.test.ts @@ -2,7 +2,6 @@ // See LICENSE.txt for license information. import { - transformChannelMembershipRecord, transformPreferenceRecord, transformReactionRecord, transformUserRecord, @@ -11,42 +10,6 @@ import {createTestConnection} from '@database/operator/utils/create_test_connect import {OperationType} from '@typings/database/enums'; describe('*** USER Prepare Records Test ***', () => { - it('=> transformChannelMembershipRecord: should return an array of type ChannelMembership', async () => { - expect.assertions(3); - - const database = await createTestConnection({databaseName: 'user_prepare_records', setActive: true}); - expect(database).toBeTruthy(); - - const preparedRecords = await transformChannelMembershipRecord({ - action: OperationType.CREATE, - database: database!, - value: { - record: undefined, - 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_user: true, - scheme_admin: false, - }, - }, - }); - - expect(preparedRecords).toBeTruthy(); - expect(preparedRecords!.collection.modelClass.name).toBe('ChannelMembershipModel'); - }); - it('=> transformPreferenceRecord: should return an array of type Preference', async () => { expect.assertions(3); diff --git a/app/database/operator/server_data_operator/transformers/user.ts b/app/database/operator/server_data_operator/transformers/user.ts index 1478e54eb0..f6774aa3a2 100644 --- a/app/database/operator/server_data_operator/transformers/user.ts +++ b/app/database/operator/server_data_operator/transformers/user.ts @@ -6,13 +6,11 @@ import {prepareBaseRecord} from '@database/operator/server_data_operator/transfo import {OperationType} from '@typings/database/enums'; import type {TransformerArgs} from '@typings/database/database'; -import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership'; import type PreferenceModel from '@typings/database/models/servers/preference'; import type ReactionModel from '@typings/database/models/servers/reaction'; import type UserModel from '@typings/database/models/servers/user'; const { - CHANNEL_MEMBERSHIP, PREFERENCE, REACTION, USER, @@ -124,30 +122,3 @@ export const transformPreferenceRecord = ({action, database, value}: Transformer }) as Promise; }; -/** - * transformChannelMembershipRecord: Prepares a record of the SERVER database 'ChannelMembership' table for update or create actions. - * @param {TransformerArgs} operator - * @param {Database} operator.database - * @param {RecordPair} operator.value - * @returns {Promise} - */ -export const transformChannelMembershipRecord = ({action, database, value}: TransformerArgs): Promise => { - const raw = value.raw as ChannelMembership; - const record = value.record as ChannelMembershipModel; - const isCreateAction = action === OperationType.CREATE; - - // If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database - const fieldsMapper = (channelMember: ChannelMembershipModel) => { - channelMember._raw.id = isCreateAction ? (raw?.id ?? channelMember.id) : record.id; - channelMember.channelId = raw.channel_id; - channelMember.userId = raw.user_id; - }; - - return prepareBaseRecord({ - action, - database, - tableName: CHANNEL_MEMBERSHIP, - value, - fieldsMapper, - }) as Promise; -}; diff --git a/app/database/operator/utils/reaction.ts b/app/database/operator/utils/reaction.ts index 3c03675c5c..030777e2ca 100644 --- a/app/database/operator/utils/reaction.ts +++ b/app/database/operator/utils/reaction.ts @@ -20,19 +20,17 @@ const {REACTION} = MM_TABLES.SERVER; * @returns {Promise<{createReactions: RawReaction[], deleteReactions: Reaction[]}>} */ export const sanitizeReactions = async ({database, post_id, rawReactions, skipSync}: SanitizeReactionsArgs) => { - const reactions = (await database.collections. - get(REACTION). + const reactions = (await database. + get(REACTION). query(Q.where('post_id', post_id)). - fetch()) as ReactionModel[]; + fetch()); // similarObjects: Contains objects that are in both the RawReaction array and in the Reaction table const similarObjects: ReactionModel[] = []; const createReactions: RecordPair[] = []; - for (let i = 0; i < rawReactions.length; i++) { - const raw = rawReactions[i]; - + for (const raw of rawReactions) { // If the reaction is not present let's add it to the db const exists = reactions.find((r) => ( r.userId === raw.user_id && @@ -49,7 +47,7 @@ export const sanitizeReactions = async ({database, post_id, rawReactions, skipSy return {createReactions, deleteReactions: []}; } - // finding out elements to delete using array subtract + // finding out elements to delete const deleteReactions = reactions. filter((reaction) => !similarObjects.includes(reaction)); diff --git a/app/queries/servers/categories.ts b/app/queries/servers/categories.ts index 25a0e00f7d..775269f471 100644 --- a/app/queries/servers/categories.ts +++ b/app/queries/servers/categories.ts @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Database, Q} from '@nozbe/watermelondb'; +import {Database, Model, Q, Query} from '@nozbe/watermelondb'; import {MM_TABLES} from '@constants/database'; @@ -113,3 +113,17 @@ export const prepareCategoryChannels = ( return undefined; } }; + +export const prepareDeleteCategory = async (category: CategoryModel): Promise => { + const preparedModels: Model[] = [category.prepareDestroyPermanently()]; + + const associatedChildren: Array> = [ + category.categoryChannels, + ]; + for await (const children of associatedChildren) { + const models = await children?.fetch?.() as Model[] | undefined; + models?.forEach((model) => preparedModels.push(model.prepareDestroyPermanently())); + } + + return preparedModels; +}; diff --git a/app/queries/servers/channel.ts b/app/queries/servers/channel.ts index 6f29effe62..ffc9d29b6a 100644 --- a/app/queries/servers/channel.ts +++ b/app/queries/servers/channel.ts @@ -96,10 +96,10 @@ export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, tea export const prepareDeleteChannel = async (channel: ChannelModel): Promise => { const preparedModels: Model[] = [channel.prepareDestroyPermanently()]; - const relations: Array> = [channel.membership, channel.info, channel.settings]; + const relations: Array> = [channel.membership, channel.info, channel.settings, channel.categoryChannel]; for await (const relation of relations) { try { - const model = await relation.fetch(); + const model = await relation?.fetch?.(); if (model) { preparedModels.push(model.prepareDestroyPermanently()); } @@ -114,14 +114,16 @@ export const prepareDeleteChannel = async (channel: ChannelModel): Promise preparedModels.push(model.prepareDestroyPermanently())); + const models = await children?.fetch?.() as Model[] | undefined; + models?.forEach((model) => preparedModels.push(model.prepareDestroyPermanently())); } - const posts = await channel.posts.fetch() as PostModel[]; - for await (const post of posts) { - const preparedPost = await prepareDeletePost(post); - preparedModels.push(...preparedPost); + const posts = await channel.posts?.fetch?.() as PostModel[] | undefined; + if (posts?.length) { + for await (const post of posts) { + const preparedPost = await prepareDeletePost(post); + preparedModels.push(...preparedPost); + } } return preparedModels; diff --git a/app/queries/servers/post.ts b/app/queries/servers/post.ts index d029a05a76..7a5782a962 100644 --- a/app/queries/servers/post.ts +++ b/app/queries/servers/post.ts @@ -16,7 +16,7 @@ export const prepareDeletePost = async (post: PostModel): Promise => { const relations: Array | Query> = [post.drafts, post.postsInThread]; for await (const relation of relations) { try { - const model = await relation.fetch(); + const model = await relation?.fetch(); if (model) { if (Array.isArray(model)) { model.forEach((m) => preparedModels.push(m.prepareDestroyPermanently())); @@ -31,8 +31,8 @@ export const prepareDeletePost = async (post: PostModel): Promise => { const associatedChildren: Array> = [post.files, post.reactions]; for await (const children of associatedChildren) { - const models = await children.fetch() as Model[]; - models.forEach((model) => preparedModels.push(model.prepareDestroyPermanently())); + const models = await children.fetch?.() as Model[] | undefined; + models?.forEach((model) => preparedModels.push(model.prepareDestroyPermanently())); } return preparedModels; diff --git a/app/queries/servers/team.ts b/app/queries/servers/team.ts index 8ac78db825..36ad740368 100644 --- a/app/queries/servers/team.ts +++ b/app/queries/servers/team.ts @@ -1,19 +1,21 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Database, Model, Q, Query} from '@nozbe/watermelondb'; +import {Database, Model, Q, Query, Relation} from '@nozbe/watermelondb'; import {Database as DatabaseConstants, Preferences} from '@constants'; import {getPreferenceValue} from '@helpers/api/preference'; import {selectDefaultTeam} from '@helpers/api/team'; import {DEFAULT_LOCALE} from '@i18n'; +import {prepareDeleteCategory} from './categories'; import {prepareDeleteChannel, queryDefaultChannelForTeam} from './channel'; import {queryPreferencesByCategoryAndName} from './preference'; import {patchTeamHistory, queryConfig, queryTeamHistory} from './system'; import {queryCurrentUser} from './user'; import type ServerDataOperator from '@database/operator/server_data_operator'; +import type CategoryModel from '@typings/database/models/servers/category'; import type ChannelModel from '@typings/database/models/servers/channel'; import type MyTeamModel from '@typings/database/models/servers/my_team'; import type TeamModel from '@typings/database/models/servers/team'; @@ -211,22 +213,16 @@ export const prepareDeleteTeam = async (team: TeamModel): Promise => { try { const preparedModels: Model[] = [team.prepareDestroyPermanently()]; - try { - const model = await team.myTeam.fetch(); - if (model) { - preparedModels.push(model.prepareDestroyPermanently()); + const relations: Array> = [team.myTeam, team.teamChannelHistory]; + for await (const relation of relations) { + try { + const model = await relation?.fetch?.(); + if (model) { + preparedModels.push(model.prepareDestroyPermanently()); + } + } catch { + // Record not found, do nothing } - } catch { - // Record not found, do nothing - } - - try { - const model = await team.teamChannelHistory.fetch(); - if (model) { - preparedModels.push(model.prepareDestroyPermanently()); - } - } catch { - // Record not found, do nothing } const associatedChildren: Array> = [ @@ -236,20 +232,34 @@ export const prepareDeleteTeam = async (team: TeamModel): Promise => { ]; for await (const children of associatedChildren) { try { - const models = await children.fetch() as Model[]; - models.forEach((model) => preparedModels.push(model.prepareDestroyPermanently())); + const models = await children.fetch?.() as Model[] | undefined; + models?.forEach((model) => preparedModels.push(model.prepareDestroyPermanently())); } catch { // Record not found, do nothing } } - const channels = await team.channels.fetch() as ChannelModel[]; - for await (const channel of channels) { - try { - const preparedChannel = await prepareDeleteChannel(channel); - preparedModels.push(...preparedChannel); - } catch { - // Record not found, do nothing + const categories = await team.categories.fetch?.() as CategoryModel[] | undefined; + if (categories?.length) { + for await (const category of categories) { + try { + const preparedCategory = await prepareDeleteCategory(category); + preparedModels.push(...preparedCategory); + } catch { + // Record not found, do nothing + } + } + } + + const channels = await team.channels.fetch?.() as ChannelModel[] | undefined; + if (channels?.length) { + for await (const channel of channels) { + try { + const preparedChannel = await prepareDeleteChannel(channel); + preparedModels.push(...preparedChannel); + } catch { + // Record not found, do nothing + } } } diff --git a/types/database/models/servers/channel.d.ts b/types/database/models/servers/channel.d.ts index 45d3a4e8e7..47e673de9f 100644 --- a/types/database/models/servers/channel.d.ts +++ b/types/database/models/servers/channel.d.ts @@ -71,5 +71,8 @@ export default class ChannelModel extends Model { /** settings: User specific settings/preferences for this channel */ settings: Relation; + /** categoryChannel: category of this channel */ + categoryChannel: Relation; + toApi = () => Channel; } diff --git a/types/database/models/servers/team.d.ts b/types/database/models/servers/team.d.ts index 5036c68255..8080cbbe92 100644 --- a/types/database/models/servers/team.d.ts +++ b/types/database/models/servers/team.d.ts @@ -1,8 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Query, Relation} from '@nozbe/watermelondb'; -import Model, {Associations} from '@nozbe/watermelondb/Model'; +import type {Query, Relation} from '@nozbe/watermelondb'; +import type Model, {Associations} from '@nozbe/watermelondb/Model'; /** * A Team houses and enables communication to happen across channels and users. @@ -41,6 +41,9 @@ export default class TeamModel extends Model { /** allowed_domains : List of domains that can join this team */ allowedDomains: string; + /** categories : All the categories associated with this team */ + categories: Query; + /** channels : All the channels associated with this team */ channels: Query;