[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
This commit is contained in:
Elias Nahum
2022-03-03 12:11:47 -03:00
committed by GitHub
parent e4ed5fe936
commit 162bc6cc3f
22 changed files with 399 additions and 290 deletions

View File

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

View File

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

View File

@@ -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<MyChannelSettingsModel>;
/** categoryChannel : Query returning the membership data for the current user if it belongs to this channel */
@immutableRelation(CATEGORY_CHANNEL, 'channel_id') categoryChannel!: Relation<CategoryChannelModel>;
toApi = (): Channel => {
return {
id: this.id,

View File

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

View File

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

View File

@@ -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<ChannelModel[]>;
handleChannelMembership: ({channelMemberships, prepareRecordsOnly}: HandleChannelMembershipArgs) => Promise<ChannelMembershipModel[]>;
handleMyChannelSettings: ({settings, prepareRecordsOnly}: HandleMyChannelSettingsArgs) => Promise<MyChannelSettingsModel[]>;
handleChannelInfo: ({channelInfos, prepareRecordsOnly}: HandleChannelInfoArgs) => Promise<ChannelInfoModel[]>;
handleMyChannel: ({channels, myChannels, prepareRecordsOnly}: HandleMyChannelArgs) => Promise<MyChannelModel[]>;
@@ -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<ChannelMembershipModel[]>}
*/
handleChannelMembership = ({channelMemberships, prepareRecordsOnly = true}: HandleChannelMembershipArgs): Promise<ChannelMembershipModel[]> => {
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;

View File

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

View File

@@ -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<Array<ReactionModel | CustomEmojiModel>>;
}
const ReactionHandler = (superclass: any) => class extends superclass {
/**
* handleReactions: Handler responsible for the Create/Update operations occurring on the Reaction table from the 'Server' schema
* @param {HandleReactionsArgs} handleReactions
* @param {ReactionsPerPost[]} handleReactions.postsReactions
* @param {boolean} handleReactions.prepareRecordsOnly
* @param {boolean} handleReactions.skipSync
* @throws DataOperatorException
* @returns {Promise<Array<(ReactionModel | CustomEmojiModel)>>}
*/
handleReactions = async ({postsReactions, prepareRecordsOnly, skipSync}: HandleReactionsArgs): Promise<ReactionModel[]> => {
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;

View File

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

View File

@@ -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<ChannelMembershipModel[]>;
handlePreferences: ({preferences, prepareRecordsOnly}: HandlePreferencesArgs) => Promise<PreferenceModel[]>;
handleReactions: ({postsReactions, prepareRecordsOnly}: HandleReactionsArgs) => Promise<Array<ReactionModel | CustomEmojiModel>>;
handleUsers: ({users, prepareRecordsOnly}: HandleUsersArgs) => Promise<UserModel[]>;
}
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<ChannelMembershipModel[]>}
*/
handleChannelMembership = ({channelMemberships, prepareRecordsOnly = true}: HandleChannelMembershipArgs): Promise<ChannelMembershipModel[]> => {
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<Array<(ReactionModel | CustomEmojiModel)>>}
*/
handleReactions = async ({postsReactions, prepareRecordsOnly, skipSync}: HandleReactionsArgs): Promise<ReactionModel[]> => {
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

View File

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

View File

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

View File

@@ -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<MyChannelModel>;
};
/**
* 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<ChannelMembershipModel>}
*/
export const transformChannelMembershipRecord = ({action, database, value}: TransformerArgs): Promise<ChannelMembershipModel> => {
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<ChannelMembershipModel>;
};

View File

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

View File

@@ -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<PreferenceModel>;
};
/**
* 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<ChannelMembershipModel>}
*/
export const transformChannelMembershipRecord = ({action, database, value}: TransformerArgs): Promise<ChannelMembershipModel> => {
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<ChannelMembershipModel>;
};

View File

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

View File

@@ -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<Model[]> => {
const preparedModels: Model[] = [category.prepareDestroyPermanently()];
const associatedChildren: Array<Query<any>> = [
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;
};

View File

@@ -96,10 +96,10 @@ export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, tea
export const prepareDeleteChannel = async (channel: ChannelModel): Promise<Model[]> => {
const preparedModels: Model[] = [channel.prepareDestroyPermanently()];
const relations: Array<Relation<Model>> = [channel.membership, channel.info, channel.settings];
const relations: Array<Relation<Model>> = [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<Model
channel.postsInChannel,
];
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()));
}
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;

View File

@@ -16,7 +16,7 @@ export const prepareDeletePost = async (post: PostModel): Promise<Model[]> => {
const relations: Array<Relation<Model> | Query<Model>> = [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<Model[]> => {
const associatedChildren: Array<Query<any>> = [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;

View File

@@ -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<Model[]> => {
try {
const preparedModels: Model[] = [team.prepareDestroyPermanently()];
try {
const model = await team.myTeam.fetch();
if (model) {
preparedModels.push(model.prepareDestroyPermanently());
const relations: Array<Relation<Model>> = [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<Query<any>> = [
@@ -236,20 +232,34 @@ export const prepareDeleteTeam = async (team: TeamModel): Promise<Model[]> => {
];
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
}
}
}

View File

@@ -71,5 +71,8 @@ export default class ChannelModel extends Model {
/** settings: User specific settings/preferences for this channel */
settings: Relation<MyChannelSettingsModel>;
/** categoryChannel: category of this channel */
categoryChannel: Relation<CategoryChanelModel>;
toApi = () => Channel;
}

View File

@@ -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<CategoryModel>;
/** channels : All the channels associated with this team */
channels: Query<ChannelModel>;