MM-30482 [Gekidou] Data Operator (#5346)

* MM_30482: Imported database and types /database folder

* MM_30482: Imported database and types /database folder

* MM_30482 : All tests are passing

* MM_30482 : Updating patch package for watermelon db

* MM_30482 : Fixing CI issue

* MM_30482 : Updating TS  complaint

* Update index.ts

* MM_30482 : Code clean up

Co-authored-by: Avinash Lingaloo <>
This commit is contained in:
Avinash Lingaloo
2021-04-22 19:16:00 +04:00
committed by GitHub
parent c25b5ab9aa
commit 78b76352c8
124 changed files with 5969 additions and 5364 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DataOperator from './handlers';
export default new DataOperator();

File diff suppressed because it is too large Load Diff

View File

@@ -1,944 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/admin/database_manager';
import {DatabaseType, OperationType} from '@typings/database/enums';
import {
operateAppRecord,
operateChannelInfoRecord,
operateChannelMembershipRecord,
operateChannelRecord,
operateCustomEmojiRecord,
operateDraftRecord,
operateFileRecord,
operateGlobalRecord,
operateGroupMembershipRecord,
operateGroupRecord,
operateGroupsInChannelRecord,
operateGroupsInTeamRecord,
operateMyChannelRecord,
operateMyChannelSettingsRecord,
operateMyTeamRecord,
operatePostInThreadRecord,
operatePostMetadataRecord,
operatePostRecord,
operatePostsInChannelRecord,
operatePreferenceRecord,
operateReactionRecord,
operateRoleRecord,
operateServersRecord,
operateSlashCommandRecord,
operateSystemRecord,
operateTeamChannelHistoryRecord,
operateTeamMembershipRecord,
operateTeamRecord,
operateTeamSearchHistoryRecord,
operateTermsOfServiceRecord,
operateUserRecord,
} from './index';
jest.mock('@database/admin/database_manager');
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** DataOperator: Operators tests ***', () => {
const createConnection = async (setActive = false) => {
const dbName = 'server_schema_connection';
const serverUrl = 'https://appv2.mattermost.com';
const database = await DatabaseManager.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
dbName,
dbType: DatabaseType.SERVER,
serverUrl,
},
});
if (setActive) {
await DatabaseManager.setActiveServerDatabase({
displayName: dbName,
serverUrl,
});
}
return database;
};
it('=> operateAppRecord: should return an array of type App', async () => {
expect.assertions(3);
const database = await DatabaseManager.getDefaultDatabase();
expect(database).toBeTruthy();
const preparedRecords = await operateAppRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
build_number: 'build-7',
created_at: 1,
version_number: 'v-1',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('App');
});
it('=> operateGlobalRecord: should return an array of type Global', async () => {
expect.assertions(3);
const database = await DatabaseManager.getDefaultDatabase();
expect(database).toBeTruthy();
const preparedRecords = await operateGlobalRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {name: 'g-n1', value: 'g-v1'},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Global');
});
it('=> operateServersRecord: should return an array of type Servers', async () => {
expect.assertions(3);
const database = await DatabaseManager.getDefaultDatabase();
expect(database).toBeTruthy();
const preparedRecords = await operateServersRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
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).toBe('Servers');
});
it('=> operateRoleRecord: should return an array of type Role', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operateRoleRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'role-1',
name: 'role-name-1',
permissions: [],
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Role');
});
it('=> operateSystemRecord: should return an array of type System', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operateSystemRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {id: 'system-1', name: 'system-name-1', value: 'system'},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('System');
});
it('=> operateTermsOfServiceRecord: should return an array of type TermsOfService', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operateTermsOfServiceRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'tos-1',
accepted_at: 1,
create_at: 1613667352029,
user_id: 'user1613667352029',
text: '',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe(
'TermsOfService',
);
});
it('=> operatePostRecord: should return an array of type Post', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operatePostRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: '8swgtrrdiff89jnsiwiip3y1eoe',
create_at: 1596032651748,
update_at: 1596032651748,
edit_at: 0,
delete_at: 0,
is_pinned: false,
user_id: 'q3mzxua9zjfczqakxdkowc6u6yy',
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
root_id: 'ps81iqbesfby8jayz7owg4yypoo',
parent_id: 'ps81iqbddesfby8jayz7owg4yypoo',
original_id: '',
message: 'Testing operator post',
type: '',
props: {},
hashtags: '',
pending_post_id: '',
reply_count: 4,
last_reply_at: 0,
participants: null,
metadata: {},
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Post');
});
it('=> operatePostInThreadRecord: should return an array of type PostsInThread', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operatePostInThreadRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'ps81iqbddesfby8jayz7owg4yypoo',
post_id: '8swgtrrdiff89jnsiwiip3y1eoe',
earliest: 1596032651748,
latest: 1597032651748,
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe(
'PostsInThread',
);
});
it('=> operateReactionRecord: should return an array of type Reaction', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operateReactionRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'ps81iqbddesfby8jayz7owg4yypoo',
user_id: 'q3mzxua9zjfczqakxdkowc6u6yy',
post_id: 'ps81iqbddesfby8jayz7owg4yypoo',
emoji_name: 'thumbsup',
create_at: 1596032651748,
update_at: 1608253011321,
delete_at: 0,
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Reaction');
});
it('=> operateFileRecord: should return an array of type File', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operateFileRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
post_id: 'ps81iqbddesfby8jayz7owg4yypoo',
name: 'test_file',
extension: '.jpg',
size: 1000,
create_at: 1609253011321,
delete_at: 1609253011321,
height: 20,
update_at: 1609253011321,
user_id: 'wqyby5r5pinxxdqhoaomtacdhc',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('File');
});
it('=> operatePostMetadataRecord: should return an array of type PostMetadata', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operatePostMetadataRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'ps81i4yypoo',
data: {},
postId: 'ps81iqbddesfby8jayz7owg4yypoo',
type: 'opengraph',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('PostMetadata');
});
it('=> operateDraftRecord: should return an array of type Draft', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operateDraftRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'ps81i4yypoo',
root_id: 'ps81iqbddesfby8jayz7owg4yypoo',
message: 'draft message',
channel_id: 'channel_idp23232e',
files: [],
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Draft');
});
it('=> operatePostsInChannelRecord: should return an array of type PostsInChannel', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operatePostsInChannelRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'ps81i4yypoo',
channel_id: 'channel_idp23232e',
earliest: 1608253011321,
latest: 1609253011321,
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe(
'PostsInChannel',
);
});
it('=> operateUserRecord: should return an array of type User', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operateUserRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: '9ciscaqbrpd6d8s68k76xb9bte',
is_bot: false,
create_at: 1599457495881,
update_at: 1607683720173,
delete_at: 0,
username: 'a.l',
auth_service: 'saml',
email: 'a.l@mattermost.com',
email_verified: true,
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,
},
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('User');
});
it('=> operatePreferenceRecord: should return an array of type Preference', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operatePreferenceRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {user_id: '9ciscaqbrpd6d8s68k76xb9bte', category: 'tutorial_step', name: '9ciscaqbrpd6d8s68k76xb9bte', value: '2'},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Preference');
});
it('=> operateTeamMembershipRecord: should return an array of type TeamMembership', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operateTeamMembershipRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
team_id: 'a',
user_id: 'ab',
roles: '3ngdqe1e7tfcbmam4qgnxp91bw',
delete_at: 0,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('TeamMembership');
});
it('=> operateCustomEmojiRecord: should return an array of type CustomEmoji', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operateCustomEmojiRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'i',
create_at: 1580913641769,
update_at: 1580913641769,
delete_at: 0,
creator_id: '4cprpki7ri81mbx8efixcsb8jo',
name: 'boomI',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('CustomEmoji');
});
it('=> operateGroupMembershipRecord: should return an array of type GroupMembership', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operateGroupMembershipRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
user_id: 'u4cprpki7ri81mbx8efixcsb8jo',
group_id: 'g4cprpki7ri81mbx8efixcsb8jo',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('GroupMembership');
});
it('=> operateChannelMembershipRecord: should return an array of type ChannelMembership', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operateChannelMembershipRecord({
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_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('ChannelMembership');
});
it('=> operateGroupRecord: should return an array of type Group', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operateGroupRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
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,
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Group');
});
it('=> operateGroupsInTeamRecord: should return an array of type GroupsInTeam', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operateGroupsInTeamRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
team_id: 'team_89',
team_display_name: '',
team_type: '',
group_id: 'group_id89',
auto_add: true,
create_at: 0,
delete_at: 0,
update_at: 0,
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('GroupsInTeam');
});
it('=> operateGroupsInChannelRecord: should return an array of type GroupsInChannel', async () => {
expect.assertions(3);
const database = await createConnection();
expect(database).toBeTruthy();
const preparedRecords = await operateGroupsInChannelRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
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,
},
},
});
expect(preparedRecords).toBeTruthy();
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');
});
});

View File

@@ -1,224 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
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,
RawSlashCommand,
RawTeam,
RawUser,
RawValue,
RecordPair,
RetrieveRecordsArgs,
SanitizePostsArgs,
SanitizeReactionsArgs,
} from '@typings/database/database';
import Reaction from '@typings/database/reaction';
import Post from '@typings/database/post';
import SlashCommand from '@typings/database/slash_command';
import Team from '@typings/database/team';
import User from '@typings/database/user';
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
* present in the orders array
* @param {SanitizePostsArgs} sanitizePosts
* @param {RawPost[]} sanitizePosts.posts
* @param {string[]} sanitizePosts.orders
*/
export const sanitizePosts = ({posts, orders}: SanitizePostsArgs) => {
const orderedPosts:RawPost[] = [];
const unOrderedPosts:RawPost[] = [];
posts.forEach((post) => {
if (post?.id && orders.includes(post.id)) {
orderedPosts.push(post);
} else {
unOrderedPosts.push(post);
}
});
return {
postsOrdered: orderedPosts,
postsUnordered: unOrderedPosts,
};
};
/**
* createPostsChain: Basically creates the 'chain of posts' using the 'orders' array; each post is linked to the other
* by the previous_post_id field.
* @param {ChainPostsArgs} chainPosts
* @param {string[]} chainPosts.orders
* @param {RawPost[]} chainPosts.rawPosts
* @param {string} chainPosts.previousPostId
* @returns {RawPost[]}
*/
export const createPostsChain = ({orders, rawPosts, previousPostId = ''}: ChainPostsArgs) => {
const posts: MatchExistingRecord[] = [];
rawPosts.forEach((post) => {
const postId = post.id;
const orderIndex = orders.findIndex((order) => {
return order === postId;
});
if (orderIndex === -1) {
// This case will not occur as we are using 'ordered' posts for this step. However, if this happens, that
// implies that we might be dealing with an unordered post and in which case we do not action on it.
} else if (orderIndex === 0) {
posts.push({record: undefined, raw: {...post, prev_post_id: previousPostId}});
} else {
posts.push({record: undefined, raw: {...post, prev_post_id: orders[orderIndex - 1]}});
}
});
return posts;
};
/**
* sanitizeReactions: Treats reactions happening on a Post. For example, a user can add/remove an emoji. Hence, this function
* tell us which reactions to create/delete in the Reaction table and which custom-emoji to create in our database.
* For more information, please have a look at https://community.mattermost.com/core/pl/rq9e8jnonpyrmnyxpuzyc4d6ko
* @param {SanitizeReactionsArgs} sanitizeReactions
* @param {Database} sanitizeReactions.database
* @param {string} sanitizeReactions.post_id
* @param {RawReaction[]} sanitizeReactions.rawReactions
* @returns {Promise<{createReactions: RawReaction[], createEmojis: {name: string}[], deleteReactions: Reaction[]}>}
*/
export const sanitizeReactions = async ({database, post_id, rawReactions}: SanitizeReactionsArgs) => {
const reactions = (await database.collections.
get(REACTION).
query(Q.where('post_id', post_id)).
fetch()) as Reaction[];
// similarObjects: Contains objects that are in both the RawReaction array and in the Reaction entity
const similarObjects: Reaction[] = [];
const createReactions: MatchExistingRecord[] = [];
const emojiSet = new Set();
for (let i = 0; i < rawReactions.length; i++) {
const rawReaction = rawReactions[i] as RawReaction;
// Do we have a similar value of rawReaction in the REACTION table?
const idxPresent = reactions.findIndex((value) => {
return (
value.userId === rawReaction.user_id &&
value.emojiName === rawReaction.emoji_name
);
});
if (idxPresent === -1) {
// So, we don't have a similar Reaction object. That one is new...so we'll create it
createReactions.push({record: undefined, raw: rawReaction});
// If that reaction is new, that implies that the emoji might also be new
emojiSet.add(rawReaction.emoji_name);
} else {
// we have a similar object in both reactions and rawReactions; we'll pop it out from both arrays
similarObjects.push(reactions[idxPresent]);
}
}
// finding out elements to delete using array subtract
const deleteReactions = reactions.
filter((reaction) => !similarObjects.includes(reaction)).
map((outCast) => outCast.prepareDestroyPermanently());
const createEmojis = Array.from(emojiSet).map((emoji) => {
return {name: emoji};
});
return {createReactions, createEmojis, deleteReactions};
};
/**
* retrieveRecords: Retrieves records from the database
* @param {RetrieveRecordsArgs} records
* @param {Database} records.database
* @param {string} records.tableName
* @param {any} records.condition
* @returns {Promise<Model[]>}
*/
export const retrieveRecords = async ({database, tableName, condition}: RetrieveRecordsArgs) => {
const records = (await database.collections.get(tableName).query(condition).fetch()) as Model[];
return records;
};
/**
* hasSimilarUpdateAt: Database Operations on some entities are expensive. As such, we would like to operate if and only if we are
* 100% sure that the records are actually different from what we already have in the database.
* @param {IdenticalRecordArgs} identicalRecord
* @param {string} identicalRecord.tableName
* @param {RecordValue} identicalRecord.newValue
* @param {Model} identicalRecord.existingRecord
* @returns {boolean}
*/
export const hasSimilarUpdateAt = ({tableName, newValue, existingRecord}: IdenticalRecordArgs) => {
const guardTables = [CHANNEL, POST, SLASH_COMMAND, TEAM, USER];
if (guardTables.includes(tableName)) {
type Raw = RawPost | RawUser | RawTeam | RawSlashCommand | RawChannel
type ExistingRecord = Post | User | Team | SlashCommand | Channel
return (newValue as Raw).update_at === (existingRecord as ExistingRecord).updateAt;
}
return false;
};
/**
* This method extracts one particular field 'fieldName' from the raw values and returns them as a string array
* @param {RangeOfValueArgs} range
* @param {string} range.fieldName
* @param {RawValue[]} range.raws
* @returns {string[]}
*/
export const getRangeOfValues = ({fieldName, raws}: RangeOfValueArgs) => {
return raws.reduce((oneOfs, current: RawValue) => {
const key = fieldName as keyof typeof current;
const value: string = current[key] as string;
if (value) {
oneOfs.push(value);
}
return oneOfs;
}, [] as string[]);
};
/**
* getRawRecordPairs: Utility method that maps over the raws array to create an array of RecordPair
* @param {any[]} raws
* @returns {{record: undefined, raw: any}[]}
*/
export const getRawRecordPairs = (raws: any[]): RecordPair[] => {
return raws.map((raw) => {
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()];
};

View File

@@ -5,10 +5,10 @@ import {Database, Q} from '@nozbe/watermelondb';
import LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs';
import {MM_TABLES} from '@constants/database';
import DefaultMigration from '@database/default/migration';
import {App, Global, Servers} from '@database/default/models';
import {defaultSchema} from '@database/default/schema';
import ServerMigration from '@database/server/migration';
import DefaultMigration from '@database/migration/default';
import {App, Global, Servers} from '@database/models/default';
import {defaultSchema} from '@database/schema/default';
import ServerMigration from '@database/migration/server';
import {
Channel,
ChannelInfo,
@@ -38,8 +38,8 @@ import {
TeamSearchHistory,
TermsOfService,
User,
} from '@database/server/models';
import {serverSchema} from '@database/server/schema';
} from '@database/models/server';
import {serverSchema} from '@database/schema/server';
import logger from '@nozbe/watermelondb/utils/common/logger';
import type {
ActiveServerDatabaseArgs,
@@ -148,7 +148,7 @@ class DatabaseManager {
return new Database({adapter, actionsEnabled, modelClasses});
} catch (e) {
// eslint-disable-next-line no-console
console.log('ERROR ==========================\n', e);
console.log('createDatabaseConnection ERROR:', e);
}
return undefined;
@@ -180,7 +180,7 @@ class DatabaseManager {
* @param {String} serverUrl
* @returns {Promise<boolean>}
*/
isServerPresent = async (serverUrl: String) => {
isServerPresent = async (serverUrl: string) => {
const allServers = await this.getAllServers();
const existingServer = allServers?.filter((server) => {
return server.url === serverUrl;

View File

@@ -8,10 +8,10 @@ import {DeviceEventEmitter, Platform} from 'react-native';
import {FileSystem} from 'react-native-unimodules';
import {MIGRATION_EVENTS, MM_TABLES} from '@constants/database';
import DefaultMigration from '@database/default/migration';
import {App, Global, Servers} from '@database/default/models';
import {defaultSchema} from '@database/default/schema';
import ServerMigration from '@database/server/migration';
import DefaultMigration from '@database/migration/default';
import {App, Global, Servers} from '@database/models/default';
import {defaultSchema} from '@database/schema/default';
import ServerMigration from '@database/migration/server';
import {
Channel,
ChannelInfo,
@@ -41,8 +41,8 @@ import {
TeamSearchHistory,
TermsOfService,
User,
} from '@database/server/models';
import {serverSchema} from '@database/server/schema';
} from '@database/models/server';
import {serverSchema} from '@database/schema/server';
import type {
ActiveServerDatabaseArgs,
DatabaseConnectionArgs,
@@ -176,7 +176,7 @@ class DatabaseManager {
* @param {String} serverUrl
* @returns {Promise<boolean>}
*/
private isServerPresent = async (serverUrl: String) => {
private isServerPresent = async (serverUrl: string) => {
const allServers = await this.getAllServers();
const existingServer = allServers?.filter((server) => {

View File

@@ -8,9 +8,9 @@ import {DatabaseInstance} from '@typings/database/database';
import {DatabaseType} from '@typings/database/enums';
import IServers from '@typings/database/servers';
import DatabaseManager from './index';
import DatabaseManager from '@database/manager';
jest.mock('./index');
jest.mock('@database/manager');
const {SERVERS} = MM_TABLES.DEFAULT;
@@ -27,6 +27,7 @@ describe('*** Database Manager tests ***', () => {
const spyOnAddServerToDefaultDatabase = jest.spyOn(DatabaseManager as any, 'addServerToDefaultDatabase');
const defaultDB = await DatabaseManager.getDefaultDatabase();
expect(defaultDB).toBeInstanceOf(Database);
expect(spyOnAddServerToDefaultDatabase).not.toHaveBeenCalledTimes(1);
});

View File

@@ -45,5 +45,5 @@ export default class ChannelInfo extends Model {
@field('purpose') purpose!: string;
/** channel : The lazy query property to the record from entity CHANNEL */
@immutableRelation(CHANNEL, 'channel_id') channel!: Relation<Channel>
@immutableRelation(CHANNEL, 'channel_id') channel!: Relation<Channel>;
}

View File

@@ -44,10 +44,10 @@ export default class ChannelMembership extends Model {
/**
* getAllChannelsForUser - Retrieves all the channels that the user is part of
*/
@lazy getAllChannelsForUser = this.collections.get(CHANNEL).query(Q.on(USER, 'id', this.userId)) as Query<Channel>
@lazy getAllChannelsForUser = this.collections.get(CHANNEL).query(Q.on(USER, 'id', this.userId)) as Query<Channel>;
/**
* getAllUsersInChannel - Retrieves all the users who are part of this channel
*/
@lazy getAllUsersInChannel = this.collections.get(USER).query(Q.on(CHANNEL, 'id', this.channelId)) as Query<User>
@lazy getAllUsersInChannel = this.collections.get(USER).query(Q.on(CHANNEL, 'id', this.channelId)) as Query<User>;
}

View File

@@ -44,10 +44,10 @@ export default class GroupMembership extends Model {
/**
* getAllGroupsForUser : Retrieves all the groups that the user is part of
*/
@lazy getAllGroupsForUser = this.collections.get(GROUP).query(Q.on(USER, 'id', this.userId)) as Query<Group>
@lazy getAllGroupsForUser = this.collections.get(GROUP).query(Q.on(USER, 'id', this.userId)) as Query<Group>;
/**
* getAllUsersInGroup : Retrieves all the users who are part of this group
*/
@lazy getAllUsersInGroup = this.collections.get(USER).query(Q.on(GROUP, 'id', this.groupId)) as Query<User>
@lazy getAllUsersInGroup = this.collections.get(USER).query(Q.on(GROUP, 'id', this.groupId)) as Query<User>;
}

View File

@@ -31,15 +31,9 @@ export default class GroupsInTeam extends Model {
/** group_id : The foreign key to the related Group record */
@field('group_id') groupId!: string;
/** member_count : The number of users in that group */
@field('member_count') memberCount!: number;
/** team_id : The foreign key to the related Team record */
@field('team_id') teamId!: string;
/** timezone_count : The number of timezones */
@field('timezone_count') timezoneCount!: number;
/** team : The related record to the parent Team model */
@immutableRelation(TEAM, 'team_id') team!: Relation<Team>;

View File

@@ -43,5 +43,5 @@ export default class MyChannel extends Model {
@field('roles') roles!: string;
/** channel : The relation pointing to entity CHANNEL */
@immutableRelation(CHANNEL, 'channel_id') channel!: Relation<Channel>
@immutableRelation(CHANNEL, 'channel_id') channel!: Relation<Channel>;
}

View File

@@ -37,5 +37,5 @@ export default class MyTeam extends Model {
@field('team_id') teamId!: string;
/** team : The relation to the entity TEAM, that this user belongs to */
@relation(MY_TEAM, 'team_id') team!: Relation<Team>
@relation(MY_TEAM, 'team_id') team!: Relation<Team>;
}

View File

@@ -44,10 +44,10 @@ export default class TeamMembership extends Model {
/**
* getAllTeamsForUser - Retrieves all the teams that the user is part of
*/
@lazy getAllTeamsForUser = this.collections.get(TEAM).query(Q.on(USER, 'id', this.userId)) as Query<Team>
@lazy getAllTeamsForUser = this.collections.get(TEAM).query(Q.on(USER, 'id', this.userId)) as Query<Team>;
/**
* getAllUsersInTeam - Retrieves all the users who are part of this team
*/
@lazy getAllUsersInTeam = this.collections.get(USER).query(Q.on(TEAM, 'id', this.teamId)) as Query<User>
@lazy getAllUsersInTeam = this.collections.get(USER).query(Q.on(TEAM, 'id', this.teamId)) as Query<User>;
}

View File

@@ -64,11 +64,7 @@ import User from '@typings/database/user';
*/
export const isRecordAppEqualToRaw = (record: App, raw: RawApp) => {
return (
raw.build_number === record.buildNumber &&
raw.created_at === record.createdAt &&
raw.version_number === record.versionNumber
);
return (raw.build_number === record.buildNumber && raw.version_number === record.versionNumber);
};
export const isRecordGlobalEqualToRaw = (record: Global, raw: RawGlobal) => {

View File

@@ -0,0 +1,302 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DataOperatorException from '@database/exceptions/data_operator_exception';
import DatabaseManager from '@database/manager';
import {DataOperator} from '@database/operator';
import {
isRecordAppEqualToRaw,
isRecordCustomEmojiEqualToRaw,
isRecordGlobalEqualToRaw,
isRecordRoleEqualToRaw,
isRecordServerEqualToRaw,
isRecordSystemEqualToRaw,
isRecordTermsOfServiceEqualToRaw,
} from '@database/operator/comparators';
import {
prepareAppRecord,
prepareCustomEmojiRecord,
prepareGlobalRecord,
prepareRoleRecord,
prepareServersRecord,
prepareSystemRecord,
prepareTermsOfServiceRecord,
} from '@database/operator/prepareRecords/general';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {RawGlobal, RawRole, RawServers, RawTermsOfService} from '@typings/database/database';
import {IsolatedEntities} from '@typings/database/enums';
jest.mock('@database/manager');
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** DataOperator: Base Handlers tests ***', () => {
it('=> HandleApp: should write to APP entity', async () => {
expect.assertions(3);
const defaultDB = await DatabaseManager.getDefaultDatabase();
expect(defaultDB).toBeTruthy();
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await DataOperator.handleIsolatedEntity({
tableName: IsolatedEntities.APP,
values: [
{
build_number: 'build-10x',
created_at: 1,
version_number: 'version-10',
},
{
build_number: 'build-11y',
created_at: 1,
version_number: 'version-11',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'version_number',
operator: prepareAppRecord,
findMatchingRecordBy: isRecordAppEqualToRaw,
rawValues: [
{
build_number: 'build-10x',
created_at: 1,
version_number: 'version-10',
},
{
build_number: 'build-11y',
created_at: 1,
version_number: 'version-11',
},
],
tableName: 'app',
prepareRecordsOnly: false,
});
});
it('=> HandleGlobal: should write to GLOBAL entity', async () => {
expect.assertions(2);
const defaultDB = await DatabaseManager.getDefaultDatabase();
expect(defaultDB).toBeTruthy();
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
const values: RawGlobal[] = [{name: 'global-1-name', value: 'global-1-value'}];
await DataOperator.handleIsolatedEntity({
tableName: IsolatedEntities.GLOBAL,
values,
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
findMatchingRecordBy: isRecordGlobalEqualToRaw,
fieldName: 'name',
operator: prepareGlobalRecord,
rawValues: values,
tableName: 'global',
prepareRecordsOnly: false,
});
});
it('=> HandleServers: should write to SERVERS entity', async () => {
expect.assertions(2);
const defaultDB = await DatabaseManager.getDefaultDatabase();
expect(defaultDB).toBeTruthy();
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
const values: RawServers[] = [
{
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,
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'url',
operator: prepareServersRecord,
findMatchingRecordBy: isRecordServerEqualToRaw,
rawValues: [
{
db_path: 'server.db',
display_name: 'community',
mention_count: 0,
unread_count: 0,
url: 'https://community.mattermost.com',
},
],
tableName: 'servers',
prepareRecordsOnly: false,
});
});
it('=> HandleRole: should write to ROLE entity', async () => {
expect.assertions(1);
await createTestConnection({databaseName: 'base_handler', setActive: true});
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
const values: RawRole[] = [
{
id: 'custom-emoji-id-1',
name: 'custom-emoji-1',
permissions: ['custom-emoji-1'],
},
];
await DataOperator.handleIsolatedEntity({
tableName: IsolatedEntities.ROLE,
values,
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'id',
operator: prepareRoleRecord,
findMatchingRecordBy: isRecordRoleEqualToRaw,
rawValues: [
{
id: 'custom-emoji-id-1',
name: 'custom-emoji-1',
permissions: ['custom-emoji-1'],
},
],
tableName: 'Role',
prepareRecordsOnly: false,
});
});
it('=> HandleCustomEmojis: should write to CUSTOM_EMOJI entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'base_handler', setActive: true});
await DataOperator.handleIsolatedEntity({
tableName: IsolatedEntities.CUSTOM_EMOJI,
values: [
{
id: 'i',
create_at: 1580913641769,
update_at: 1580913641769,
delete_at: 0,
creator_id: '4cprpki7ri81mbx8efixcsb8jo',
name: 'boomI',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'id',
rawValues: [
{
id: 'i',
create_at: 1580913641769,
update_at: 1580913641769,
delete_at: 0,
creator_id: '4cprpki7ri81mbx8efixcsb8jo',
name: 'boomI',
},
],
tableName: 'CustomEmoji',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordCustomEmojiEqualToRaw,
operator: prepareCustomEmojiRecord,
});
});
it('=> HandleSystem: should write to SYSTEM entity', async () => {
expect.assertions(1);
await createTestConnection({databaseName: 'base_handler', setActive: true});
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
const values = [{id: 'system-id-1', name: 'system-1', value: 'system-1'}];
await DataOperator.handleIsolatedEntity({
tableName: IsolatedEntities.SYSTEM,
values,
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
findMatchingRecordBy: isRecordSystemEqualToRaw,
fieldName: 'id',
operator: prepareSystemRecord,
rawValues: values,
tableName: 'System',
prepareRecordsOnly: false,
});
});
it('=> HandleTermsOfService: should write to TERMS_OF_SERVICE entity', async () => {
expect.assertions(1);
await createTestConnection({databaseName: 'base_handler', setActive: true});
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
const values: RawTermsOfService[] = [
{
id: 'tos-1',
accepted_at: 1,
create_at: 1613667352029,
user_id: 'user1613667352029',
text: '',
},
];
await DataOperator.handleIsolatedEntity({
tableName: IsolatedEntities.TERMS_OF_SERVICE,
values,
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
findMatchingRecordBy: isRecordTermsOfServiceEqualToRaw,
fieldName: 'id',
operator: prepareTermsOfServiceRecord,
rawValues: values,
tableName: 'TermsOfService',
prepareRecordsOnly: false,
});
});
it('=> No table name: should not call executeInDatabase if tableName is invalid', async () => {
expect.assertions(2);
const defaultDB = await DatabaseManager.getDefaultDatabase();
expect(defaultDB).toBeTruthy();
await expect(
DataOperator.handleIsolatedEntity({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
tableName: 'INVALID_TABLE_NAME',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
values: [{id: 'tos-1', accepted_at: 1}],
}),
).rejects.toThrow(DataOperatorException);
});
});

View File

@@ -0,0 +1,456 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database, Q} from '@nozbe/watermelondb';
import Model from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import DataOperatorException from '@database/exceptions/data_operator_exception';
import DatabaseConnectionException from '@database/exceptions/database_connection_exception';
import DatabaseManager from '@database/manager';
import {
isRecordAppEqualToRaw,
isRecordCustomEmojiEqualToRaw,
isRecordGlobalEqualToRaw,
isRecordRoleEqualToRaw,
isRecordServerEqualToRaw,
isRecordSystemEqualToRaw,
isRecordTermsOfServiceEqualToRaw,
} from '@database/operator/comparators';
import {
prepareAppRecord,
prepareCustomEmojiRecord,
prepareGlobalRecord,
prepareRoleRecord,
prepareServersRecord,
prepareSystemRecord,
prepareTermsOfServiceRecord,
} from '@database/operator/prepareRecords/general';
import {
getRangeOfValues,
getRawRecordPairs,
getUniqueRawsBy,
hasSimilarUpdateAt,
retrieveRecords,
} from '@database/operator/utils/general';
import {
BatchOperationsArgs,
DatabaseInstance,
HandleEntityRecordsArgs,
HandleIsolatedEntityArgs,
PrepareForDatabaseArgs,
PrepareRecordsArgs,
ProcessInputsArgs,
RawValue,
RecordPair,
} from '@typings/database/database';
import {IsolatedEntities, OperationType} from '@typings/database/enums';
export interface BaseHandlerMix {
activeDatabase: Database;
getActiveDatabase: () => DatabaseInstance;
setActiveDatabase: (database: Database) => void;
handleIsolatedEntity: ({tableName, values, prepareRecordsOnly}: HandleIsolatedEntityArgs) => boolean | Model[];
handleEntityRecords: ({findMatchingRecordBy, fieldName, operator, rawValues, tableName, prepareRecordsOnly}: HandleEntityRecordsArgs) => Promise<null | Model[]>;
processInputs: ({rawValues, tableName, findMatchingRecordBy, fieldName}: ProcessInputsArgs) => Promise<{ createRaws: RecordPair[]; updateRaws: RecordPair[] }>;
batchOperations: ({database, models}: BatchOperationsArgs) => Promise<void>;
prepareRecords: ({database, tableName, createRaws, updateRaws, recordOperator}: PrepareRecordsArgs) => Promise<Model[]>;
executeInDatabase: ({createRaws, recordOperator, tableName, updateRaws}: PrepareForDatabaseArgs) => Promise<void>;
getDatabase: (tableName: string) => Database;
getDefaultDatabase: () => Promise<Database>;
getServerDatabase: () => Promise<Database>;
}
class BaseHandler {
/**
* activeDatabase : In a multi-server configuration, this connection will be used by WebSockets and other parties to update databases other than the active one.
* @type {DatabaseInstance}
*/
activeDatabase: DatabaseInstance;
constructor(serverDatabase?: Database) {
this.activeDatabase = serverDatabase;
}
/**
* getActiveDatabase : getter for the activeDatabase
* @returns {DatabaseInstance}
*/
getActiveDatabase = () => this.activeDatabase;
/**
* setActiveDatabase: setter for the activeDatabase
* @param {} database
*/
setActiveDatabase = (database: Database) => {
this.activeDatabase = database;
};
/**
* handleIsolatedEntity: Handler responsible for the Create/Update operations on the isolated entities as described
* by the IsolatedEntities enum
* @param {HandleIsolatedEntityArgs} isolatedEntityArgs
* @param {IsolatedEntities} isolatedEntityArgs.tableName
* @param {boolean} isolatedEntityArgs.prepareRecordsOnly
* @param {RawValue} isolatedEntityArgs.values
* @throws DataOperatorException
* @returns {Model[] | boolean}
*/
handleIsolatedEntity = async ({tableName, values, prepareRecordsOnly = true}: HandleIsolatedEntityArgs) => {
let findMatchingRecordBy;
let fieldName;
let operator;
let rawValues;
if (!values.length) {
throw new DataOperatorException(
`An empty "values" array has been passed to the handleIsolatedEntity method for entity ${tableName}`,
);
}
switch (tableName) {
case IsolatedEntities.APP: {
findMatchingRecordBy = isRecordAppEqualToRaw;
fieldName = 'version_number';
operator = prepareAppRecord;
rawValues = getUniqueRawsBy({raws: values, key: 'version_number'});
break;
}
case IsolatedEntities.CUSTOM_EMOJI: {
findMatchingRecordBy = isRecordCustomEmojiEqualToRaw;
fieldName = 'id';
operator = prepareCustomEmojiRecord;
rawValues = getUniqueRawsBy({raws: values, key: 'id'});
break;
}
case IsolatedEntities.GLOBAL: {
findMatchingRecordBy = isRecordGlobalEqualToRaw;
fieldName = 'name';
operator = prepareGlobalRecord;
rawValues = getUniqueRawsBy({raws: values, key: 'name'});
break;
}
case IsolatedEntities.ROLE: {
findMatchingRecordBy = isRecordRoleEqualToRaw;
fieldName = 'id';
operator = prepareRoleRecord;
rawValues = getUniqueRawsBy({raws: values, key: 'id'});
break;
}
case IsolatedEntities.SERVERS: {
findMatchingRecordBy = isRecordServerEqualToRaw;
fieldName = 'url';
operator = prepareServersRecord;
rawValues = getUniqueRawsBy({raws: values, key: 'display_name'});
break;
}
case IsolatedEntities.SYSTEM: {
findMatchingRecordBy = isRecordSystemEqualToRaw;
fieldName = 'id';
operator = prepareSystemRecord;
rawValues = getUniqueRawsBy({raws: values, key: 'id'});
break;
}
case IsolatedEntities.TERMS_OF_SERVICE: {
findMatchingRecordBy = isRecordTermsOfServiceEqualToRaw;
fieldName = 'id';
operator = prepareTermsOfServiceRecord;
rawValues = getUniqueRawsBy({raws: values, key: 'id'});
break;
}
default: {
throw new DataOperatorException(
`handleIsolatedEntity was called with an invalid table name ${tableName}`,
);
}
}
if (fieldName && findMatchingRecordBy) {
const records = await this.handleEntityRecords({
fieldName,
findMatchingRecordBy,
operator,
prepareRecordsOnly,
rawValues,
tableName,
});
return prepareRecordsOnly && records?.length && records;
}
return false;
};
/**
* handleEntityRecords : Utility that processes some entities' data against values already present in the database so as to avoid duplicity.
* @param {HandleEntityRecordsArgs} handleEntityArgs
* @param {(existing: Model, newElement: RawValue) => boolean} handleEntityArgs.findMatchingRecordBy
* @param {string} handleEntityArgs.fieldName
* @param {(DataFactoryArgs) => Promise<Model>} handleEntityArgs.operator
* @param {RawValue[]} handleEntityArgs.rawValues
* @param {string} handleEntityArgs.tableName
* @returns {Promise<null | Model[]>}
*/
handleEntityRecords = async ({findMatchingRecordBy, fieldName, operator, rawValues, tableName, prepareRecordsOnly = true}: HandleEntityRecordsArgs) => {
if (!rawValues.length) {
return null;
}
const {createRaws, updateRaws} = await this.processInputs({
rawValues,
tableName,
findMatchingRecordBy,
fieldName,
});
const database = await this.getDatabase(tableName);
const models = await this.prepareRecords({
database,
tableName,
createRaws,
updateRaws,
recordOperator: operator,
});
if (prepareRecordsOnly) {
return models;
}
if (models?.length > 0) {
await this.batchOperations({database, models});
}
return null;
};
/**
* 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} inputsArg
* @param {RawValue[]} inputsArg.rawValues
* @param {string} inputsArg.tableName
* @param {string} inputsArg.fieldName
* @param {(existing: Model, newElement: RawValue) => boolean} inputsArg.findMatchingRecordBy
* @returns {Promise<{createRaws: RecordPair[], updateRaws: RecordPair[]} | {createRaws: RecordPair[], updateRaws: RecordPair[]}>}
*/
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,
});
const database = await this.getDatabase(tableName);
const existingRecords = await retrieveRecords({
database,
tableName,
condition: Q.where(fieldName, Q.oneOf(columnValues)),
});
const createRaws: RecordPair[] = [];
const updateRaws: RecordPair[] = [];
if (existingRecords.length > 0) {
rawValues.forEach((newElement: RawValue) => {
const findIndex = existingRecords.findIndex((existing) => {
return findMatchingRecordBy(existing, newElement);
});
// We found a record in the database that matches this element; hence, we'll proceed for an UPDATE operation
if (findIndex > -1) {
const existingRecord = existingRecords[findIndex];
// Some raw value has an update_at field. We'll proceed to update only if the update_at value is different from the record's value in database
const isUpdateAtSimilar = hasSimilarUpdateAt({
tableName,
existingRecord,
newValue: newElement,
});
if (!isUpdateAtSimilar) {
return updateRaws.push({
record: existingRecord,
raw: newElement,
});
}
} else {
// This RawValue is not present in the database; hence, we need to create it
return createRaws.push({record: undefined, raw: newElement});
}
return null;
});
return {
createRaws,
updateRaws,
};
}
return {
createRaws: getRawRecordPairs(rawValues),
updateRaws,
};
};
/**
* batchOperations: Accepts an instance of Database (either Default or Server) and an array of
* prepareCreate/prepareUpdate 'models' and executes the actions on the database.
* @param {BatchOperationsArgs} operation
* @param {Database} operation.database
* @param {Array} operation.models
* @throws {DataOperatorException}
* @returns {Promise<void>}
*/
batchOperations = async ({database, models}: BatchOperationsArgs) => {
try {
if (models.length > 0) {
await database.action(async () => {
await database.batch(...models);
});
}
} catch (e) {
throw new DataOperatorException('batchOperations error ', e);
}
};
/**
* prepareRecords: Utility method that actually calls the operators for the handlers
* @param {PrepareRecordsArgs} prepareRecord
* @param {Database} prepareRecord.database
* @param {string} prepareRecord.tableName
* @param {RawValue[]} prepareRecord.createRaws
* @param {RawValue[]} prepareRecord.updateRaws
* @param {(DataFactoryArgs) => Promise<Model>;} prepareRecord.recordOperator
* @throws {DataOperatorException}
* @returns {Promise<Model[]>}
*/
prepareRecords = async ({database, tableName, createRaws, updateRaws, recordOperator}: PrepareRecordsArgs) => {
if (!database) {
throw new DataOperatorException(
'prepareRecords accepts only rawPosts of type RawValue[] or valid database connection',
);
}
let preparedRecords: Promise<Model>[] = [];
// create operation
if (createRaws?.length) {
const recordPromises = createRaws.map(
(createRecord: RecordPair) => {
return recordOperator({
database,
tableName,
value: createRecord,
action: OperationType.CREATE,
});
},
);
preparedRecords = preparedRecords.concat(recordPromises);
}
// update operation
if (updateRaws?.length) {
const recordPromises = updateRaws.map(
(updateRecord: RecordPair) => {
return recordOperator({
database,
tableName,
value: updateRecord,
action: OperationType.UPDATE,
});
},
);
preparedRecords = preparedRecords.concat(recordPromises);
}
const results = await Promise.all(preparedRecords);
return results;
};
/**
* executeInDatabase: Handles the Create/Update operations on an entity.
* @param {PrepareForDatabaseArgs} executeInDatabase
* @param {string} executeInDatabase.tableName
* @param {RecordValue[]} executeInDatabase.createRaws
* @param {RecordValue[]} executeInDatabase.updateRaws
* @param {(DataFactoryArgs) => Promise<Model>} executeInDatabase.recordOperator
* @returns {Promise<void>}
*/
executeInDatabase = async ({createRaws, recordOperator, tableName, updateRaws}: PrepareForDatabaseArgs) => {
const database = await this.getDatabase(tableName);
const models = await this.prepareRecords({
database,
tableName,
createRaws,
updateRaws,
recordOperator,
});
if (models?.length > 0) {
await this.batchOperations({database, models});
}
};
/**
* getDatabase: Based on the table's name, it will return a database instance either from the 'DEFAULT' database or
* the 'SERVER' database
* @param {string} tableName
* @returns {Promise<Database>}
*/
getDatabase = async (tableName: string) => {
const isDefaultConnection = Object.values(MM_TABLES.DEFAULT).some((tbName) => {
return tableName === tbName;
});
const promise = isDefaultConnection ? this.getDefaultDatabase : this.getServerDatabase;
const connection = await promise();
return connection;
};
/**
* getDefaultDatabase: Returns the default database
* @throws {DatabaseConnectionException}
* @returns {Promise<Database>}
*/
getDefaultDatabase = async () => {
const connection = await DatabaseManager.getDefaultDatabase();
if (connection === undefined) {
throw new DatabaseConnectionException(
'An error occurred while retrieving the default database',
'',
);
}
return connection;
};
/**
* getServerDatabase: Returns the current active server database (multi-server support)
* @throws {DatabaseConnectionException}
* @returns {Promise<Database>}
*/
getServerDatabase = async () => {
// Third parties trying to update the database
if (this.activeDatabase) {
return this.activeDatabase;
}
// NOTE: here we are getting the active server directly as in a multi-server support system, the current
// active server connection will already be set on application init
const connection = await DatabaseManager.getActiveServerDatabase();
if (connection === undefined) {
throw new DatabaseConnectionException(
'An error occurred while retrieving the server database',
'',
);
}
return connection;
};
}
export default BaseHandler;

View File

@@ -0,0 +1,220 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {DataOperator} from '@database/operator';
import {
isRecordChannelEqualToRaw,
isRecordChannelInfoEqualToRaw,
isRecordMyChannelEqualToRaw,
isRecordMyChannelSettingsEqualToRaw,
} from '@database/operator/comparators';
import {
prepareChannelInfoRecord,
prepareChannelRecord,
prepareMyChannelRecord,
prepareMyChannelSettingsRecord,
} from '@database/operator/prepareRecords/channel';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
jest.mock('@database/manager');
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** Operator: Channel Handlers tests ***', () => {
it('=> HandleChannel: should write to CHANNEL entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'channel_handler', setActive: true});
await DataOperator.handleChannel({
channels: [
{
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,
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'id',
rawValues: [
{
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',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordChannelEqualToRaw,
operator: prepareChannelRecord,
});
});
it('=> HandleMyChannelSettings: should write to MY_CHANNEL_SETTINGS entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'channel_handler', setActive: true});
await DataOperator.handleMyChannelSettings({
settings: [
{
channel_id: 'c',
notify_props: {
desktop: 'all',
desktop_sound: true,
email: true,
first_name: true,
mention_keys: '',
push: 'mention',
channel: true,
},
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'channel_id',
rawValues: [
{
channel_id: 'c',
notify_props: {
desktop: 'all',
desktop_sound: true,
email: true,
first_name: true,
mention_keys: '',
push: 'mention',
channel: true,
},
},
],
tableName: 'MyChannelSettings',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordMyChannelSettingsEqualToRaw,
operator: prepareMyChannelSettingsRecord,
});
});
it('=> HandleChannelInfo: should write to CHANNEL_INFO entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'channel_handler', setActive: true});
await DataOperator.handleChannelInfo({
channelInfos: [
{
channel_id: 'c',
guest_count: 10,
header: 'channel info header',
member_count: 10,
pinned_post_count: 3,
purpose: 'sample channel ',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'channel_id',
rawValues: [
{
channel_id: 'c',
guest_count: 10,
header: 'channel info header',
member_count: 10,
pinned_post_count: 3,
purpose: 'sample channel ',
},
],
tableName: 'ChannelInfo',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordChannelInfoEqualToRaw,
operator: prepareChannelInfoRecord,
});
});
it('=> HandleMyChannel: should write to MY_CHANNEL entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'channel_handler', setActive: true});
await DataOperator.handleMyChannel({
myChannels: [
{
channel_id: 'c',
last_post_at: 1617311494451,
last_viewed_at: 1617311494451,
mentions_count: 3,
message_count: 10,
roles: 'guest',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'channel_id',
rawValues: [
{
channel_id: 'c',
last_post_at: 1617311494451,
last_viewed_at: 1617311494451,
mentions_count: 3,
message_count: 10,
roles: 'guest',
},
],
tableName: 'MyChannel',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordMyChannelEqualToRaw,
operator: prepareMyChannelRecord,
});
});
});

View File

@@ -0,0 +1,168 @@
// 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 {
isRecordChannelEqualToRaw,
isRecordChannelInfoEqualToRaw,
isRecordMyChannelEqualToRaw,
isRecordMyChannelSettingsEqualToRaw,
} from '@database/operator/comparators';
import {
prepareChannelInfoRecord,
prepareChannelRecord,
prepareMyChannelRecord,
prepareMyChannelSettingsRecord,
} from '@database/operator/prepareRecords/channel';
import {getUniqueRawsBy} from '@database/operator/utils/general';
import Channel from '@typings/database/channel';
import ChannelInfo from '@typings/database/channel_info';
import {
HandleChannelArgs,
HandleChannelInfoArgs,
HandleMyChannelArgs,
HandleMyChannelSettingsArgs,
} from '@typings/database/database';
import MyChannel from '@typings/database/my_channel';
import MyChannelSettings from '@typings/database/my_channel_settings';
const {
CHANNEL,
CHANNEL_INFO,
MY_CHANNEL,
MY_CHANNEL_SETTINGS,
} = MM_TABLES.SERVER;
export interface ChannelHandlerMix {
handleChannel: ({channels, prepareRecordsOnly}: HandleChannelArgs) => Channel[] | boolean;
handleMyChannelSettings: ({settings, prepareRecordsOnly}: HandleMyChannelSettingsArgs) => MyChannelSettings[] | boolean;
handleChannelInfo: ({channelInfos, prepareRecordsOnly}: HandleChannelInfoArgs) => ChannelInfo[] | boolean;
handleMyChannel: ({myChannels, prepareRecordsOnly}: HandleMyChannelArgs) => MyChannel[] | boolean;
}
const ChannelHandler = (superclass: any) => class extends superclass {
/**
* handleChannel: Handler responsible for the Create/Update operations occurring on the CHANNEL entity from the 'Server' schema
* @param {HandleChannelArgs} channelsArgs
* @param {RawChannel[]} channelsArgs.channels
* @param {boolean} channelsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {Channel[]|boolean}
*/
handleChannel = async ({channels, prepareRecordsOnly = true}: HandleChannelArgs) => {
if (!channels.length) {
throw new DataOperatorException(
'An empty "channels" array has been passed to the handleChannel method',
);
}
const rawValues = getUniqueRawsBy({raws: channels, key: 'id'});
const records = await this.handleEntityRecords({
fieldName: 'id',
findMatchingRecordBy: isRecordChannelEqualToRaw,
operator: prepareChannelRecord,
prepareRecordsOnly,
rawValues,
tableName: CHANNEL,
});
return prepareRecordsOnly && records?.length && records;
};
/**
* handleMyChannelSettings: Handler responsible for the Create/Update operations occurring on the MY_CHANNEL_SETTINGS entity from the 'Server' schema
* @param {HandleMyChannelSettingsArgs} settingsArgs
* @param {RawMyChannelSettings[]} settingsArgs.settings
* @param {boolean} settingsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {MyChannelSettings[]| boolean}
*/
handleMyChannelSettings = async ({settings, prepareRecordsOnly = true}: HandleMyChannelSettingsArgs) => {
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'});
const records = await this.handleEntityRecords({
fieldName: 'channel_id',
findMatchingRecordBy: isRecordMyChannelSettingsEqualToRaw,
operator: prepareMyChannelSettingsRecord,
prepareRecordsOnly,
rawValues,
tableName: MY_CHANNEL_SETTINGS,
});
return prepareRecordsOnly && records?.length && records;
};
/**
* handleChannelInfo: Handler responsible for the Create/Update operations occurring on the CHANNEL_INFO entity from the 'Server' schema
* @param {HandleChannelInfoArgs} channelInfosArgs
* @param {RawChannelInfo[]} channelInfosArgs.channelInfos
* @param {boolean} channelInfosArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {ChannelInfo[]| boolean}
*/
handleChannelInfo = async ({channelInfos, prepareRecordsOnly = true}: HandleChannelInfoArgs) => {
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',
});
const records = await this.handleEntityRecords({
fieldName: 'channel_id',
findMatchingRecordBy: isRecordChannelInfoEqualToRaw,
operator: prepareChannelInfoRecord,
prepareRecordsOnly,
rawValues,
tableName: CHANNEL_INFO,
});
return prepareRecordsOnly && records?.length && records;
};
/**
* handleMyChannel: Handler responsible for the Create/Update operations occurring on the MY_CHANNEL entity from the 'Server' schema
* @param {HandleMyChannelArgs} myChannelsArgs
* @param {RawMyChannel[]} myChannelsArgs.myChannels
* @param {boolean} myChannelsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {MyChannel[]| boolean}
*/
handleMyChannel = async ({myChannels, prepareRecordsOnly = true}: HandleMyChannelArgs) => {
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',
});
const records = await this.handleEntityRecords({
fieldName: 'channel_id',
findMatchingRecordBy: isRecordMyChannelEqualToRaw,
operator: prepareMyChannelRecord,
prepareRecordsOnly,
rawValues,
tableName: MY_CHANNEL,
});
return prepareRecordsOnly && records?.length && records;
};
};
export default ChannelHandler;

View File

@@ -0,0 +1,205 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {DataOperator} from '@database/operator';
import {
isRecordGroupEqualToRaw,
isRecordGroupMembershipEqualToRaw,
isRecordGroupsInChannelEqualToRaw,
isRecordGroupsInTeamEqualToRaw,
} from '@database/operator/comparators';
import {
prepareGroupMembershipRecord,
prepareGroupRecord,
prepareGroupsInChannelRecord,
prepareGroupsInTeamRecord,
} from '@database/operator/prepareRecords/group';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
jest.mock('@database/manager');
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** Operator: Group Handlers tests ***', () => {
it('=> HandleGroup: should write to GROUP entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'group_handler', setActive: true});
await DataOperator.handleGroup({
groups: [
{
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,
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'name',
rawValues: [
{
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',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupEqualToRaw,
operator: prepareGroupRecord,
});
});
it('=> HandleGroupsInTeam: should write to GROUPS_IN_TEAM entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'group_handler', setActive: true});
await DataOperator.handleGroupsInTeam({
groupsInTeams: [
{
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,
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'group_id',
rawValues: [
{
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',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupsInTeamEqualToRaw,
operator: prepareGroupsInTeamRecord,
});
});
it('=> HandleGroupsInChannel: should write to GROUPS_IN_CHANNEL entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'group_handler', setActive: true});
await DataOperator.handleGroupsInChannel({
groupsInChannels: [
{
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,
member_count: 0,
timezone_count: 0,
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'group_id',
rawValues: [
{
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,
member_count: 0,
timezone_count: 0,
},
],
tableName: 'GroupsInChannel',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupsInChannelEqualToRaw,
operator: prepareGroupsInChannelRecord,
});
});
it('=> HandleGroupMembership: should write to GROUP_MEMBERSHIP entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'group_handler', setActive: true});
await DataOperator.handleGroupMembership({
groupMemberships: [
{
user_id: 'u4cprpki7ri81mbx8efixcsb8jo',
group_id: 'g4cprpki7ri81mbx8efixcsb8jo',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
rawValues: [
{
user_id: 'u4cprpki7ri81mbx8efixcsb8jo',
group_id: 'g4cprpki7ri81mbx8efixcsb8jo',
},
],
tableName: 'GroupMembership',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupMembershipEqualToRaw,
operator: prepareGroupMembershipRecord,
});
});
});

View File

@@ -0,0 +1,162 @@
// 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 {
isRecordGroupEqualToRaw,
isRecordGroupMembershipEqualToRaw,
isRecordGroupsInChannelEqualToRaw,
isRecordGroupsInTeamEqualToRaw,
} from '@database/operator/comparators';
import {
prepareGroupMembershipRecord,
prepareGroupRecord,
prepareGroupsInChannelRecord,
prepareGroupsInTeamRecord,
} from '@database/operator/prepareRecords/group';
import {getUniqueRawsBy} from '@database/operator/utils/general';
import {
HandleGroupArgs,
HandleGroupMembershipArgs,
HandleGroupsInChannelArgs,
HandleGroupsInTeamArgs,
} from '@typings/database/database';
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';
const {
GROUP,
GROUPS_IN_CHANNEL,
GROUPS_IN_TEAM,
GROUP_MEMBERSHIP,
} = MM_TABLES.SERVER;
export interface GroupHandlerMix {
handleGroupMembership : ({groupMemberships, prepareRecordsOnly}: HandleGroupMembershipArgs) => GroupMembership[] | boolean,
handleGroup : ({groups, prepareRecordsOnly}: HandleGroupArgs) => Group[] | boolean,
handleGroupsInTeam : ({groupsInTeams, prepareRecordsOnly} : HandleGroupsInTeamArgs) => GroupsInTeam[] | boolean,
handleGroupsInChannel : ({groupsInChannels, prepareRecordsOnly}: HandleGroupsInChannelArgs) => GroupsInChannel[] | boolean
}
const GroupHandler = (superclass: any) => class extends superclass {
/**
* handleGroupMembership: Handler responsible for the Create/Update operations occurring on the GROUP_MEMBERSHIP entity from the 'Server' schema
* @param {HandleGroupMembershipArgs} groupMembershipsArgs
* @param {RawGroupMembership[]} groupMembershipsArgs.groupMemberships
* @param {boolean} groupMembershipsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {GroupMembership[] | boolean}
*/
handleGroupMembership = async ({groupMemberships, prepareRecordsOnly = true}: HandleGroupMembershipArgs) => {
if (!groupMemberships.length) {
throw new DataOperatorException(
'An empty "groupMemberships" array has been passed to the handleGroupMembership method',
);
}
const rawValues = getUniqueRawsBy({raws: groupMemberships, key: 'group_id'});
const records = await this.handleEntityRecords({
fieldName: 'user_id',
findMatchingRecordBy: isRecordGroupMembershipEqualToRaw,
operator: prepareGroupMembershipRecord,
prepareRecordsOnly,
rawValues,
tableName: GROUP_MEMBERSHIP,
});
return prepareRecordsOnly && records?.length && records;
};
/**
* handleGroup: Handler responsible for the Create/Update operations occurring on the GROUP entity from the 'Server' schema
* @param {HandleGroupArgs} groupsArgs
* @param {RawGroup[]} groupsArgs.groups
* @param {boolean} groupsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {Group[] | boolean}
*/
handleGroup = async ({groups, prepareRecordsOnly = true}: HandleGroupArgs) => {
if (!groups.length) {
throw new DataOperatorException(
'An empty "groups" array has been passed to the handleGroup method',
);
}
const rawValues = getUniqueRawsBy({raws: groups, key: 'name'});
const records = await this.handleEntityRecords({
fieldName: 'name',
findMatchingRecordBy: isRecordGroupEqualToRaw,
operator: prepareGroupRecord,
prepareRecordsOnly,
rawValues,
tableName: GROUP,
});
return prepareRecordsOnly && records?.length && records;
};
/**
* handleGroupsInTeam: Handler responsible for the Create/Update operations occurring on the GROUPS_IN_TEAM entity from the 'Server' schema
* @param {HandleGroupsInTeamArgs} groupsInTeamsArgs
* @param {RawGroupsInTeam[]} groupsInTeamsArgs.groupsInTeams
* @param {boolean} groupsInTeamsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {GroupsInTeam[] | boolean}
*/
handleGroupsInTeam = async ({groupsInTeams, prepareRecordsOnly = true} : HandleGroupsInTeamArgs) => {
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'});
const records = await this.handleEntityRecords({
fieldName: 'group_id',
findMatchingRecordBy: isRecordGroupsInTeamEqualToRaw,
operator: prepareGroupsInTeamRecord,
prepareRecordsOnly,
rawValues,
tableName: GROUPS_IN_TEAM,
});
return prepareRecordsOnly && records?.length && records;
};
/**
* handleGroupsInChannel: Handler responsible for the Create/Update operations occurring on the GROUPS_IN_CHANNEL entity from the 'Server' schema
* @param {HandleGroupsInChannelArgs} groupsInChannelsArgs
* @param {RawGroupsInChannel[]} groupsInChannelsArgs.groupsInChannels
* @param {boolean} groupsInChannelsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {GroupsInChannel[] | boolean}
*/
handleGroupsInChannel = async ({groupsInChannels, prepareRecordsOnly = true}: HandleGroupsInChannelArgs) => {
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'});
const records = await this.handleEntityRecords({
fieldName: 'group_id',
findMatchingRecordBy: isRecordGroupsInChannelEqualToRaw,
operator: prepareGroupsInChannelRecord,
prepareRecordsOnly,
rawValues,
tableName: GROUPS_IN_CHANNEL,
});
return prepareRecordsOnly && records?.length && records;
};
};
export default GroupHandler;

View File

@@ -0,0 +1,342 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {DataOperator} from '@database/operator';
import {isRecordDraftEqualToRaw} from '@database/operator/comparators';
import {prepareDraftRecord} from '@database/operator/prepareRecords/post';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
jest.mock('@database/manager');
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** Operator: Post Handlers tests ***', () => {
it('=> HandleDraft: should write to the Draft entity', async () => {
expect.assertions(1);
await createTestConnection({databaseName: 'post_handler', setActive: true});
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
const values = [
{
channel_id: '4r9jmr7eqt8dxq3f9woypzurrychannelid',
files: [
{
user_id: 'user_id',
post_id: 'post_id',
create_at: 123,
update_at: 456,
delete_at: 789,
name: 'an_image',
extension: 'jpg',
size: 10,
mime_type: 'image',
width: 10,
height: 10,
has_preview_image: false,
clientId: 'clientId',
},
],
message: 'test draft message for post',
root_id: '',
},
];
await DataOperator.handleDraft({drafts: values, prepareRecordsOnly: false});
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
findMatchingRecordBy: isRecordDraftEqualToRaw,
fieldName: 'channel_id',
operator: prepareDraftRecord,
rawValues: values,
tableName: 'Draft',
prepareRecordsOnly: false,
});
});
it('=> HandlePosts: should write to Post and its sub-child entities', async () => {
expect.assertions(12);
const posts = [
{
id: '8swgtrrdiff89jnsiwiip3y1eoe',
create_at: 1596032651747,
update_at: 1596032651747,
edit_at: 0,
delete_at: 0,
is_pinned: false,
user_id: 'q3mzxua9zjfczqakxdkowc6u6yy',
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
root_id: '',
parent_id: 'ps81iqbddesfby8jayz7owg4yypoo',
original_id: '',
message: "I'll second these kudos! Thanks m!",
type: '',
props: {},
hashtags: '',
pending_post_id: '',
reply_count: 4,
last_reply_at: 0,
participants: null,
metadata: {
images: {
'https://community-release.mattermost.com/api/v4/image?url=https%3A%2F%2Favatars1.githubusercontent.com%2Fu%2F6913320%3Fs%3D400%26v%3D4': {
width: 400,
height: 400,
format: 'png',
frame_count: 0,
},
},
reactions: [
{
user_id: 'njic1w1k5inefp848jwk6oukio',
post_id: 'a7ebyw883trm884p1qcgt8yw4a',
emoji_name: 'clap',
create_at: 1608252965442,
update_at: 1608252965442,
delete_at: 0,
},
],
embeds: [
{
type: 'opengraph',
url: 'https://github.com/mickmister/mattermost-plugin-default-theme',
data: {
type: 'object',
url: 'https://github.com/mickmister/mattermost-plugin-default-theme',
title: 'mickmister/mattermost-plugin-default-theme',
description: 'Contribute to mickmister/mattermost-plugin-default-theme development by creating an account on GitHub.',
determiner: '',
site_name: 'GitHub',
locale: '',
locales_alternate: null,
images: [
{
url: '',
secure_url: 'https://community-release.mattermost.com/api/v4/image?url=https%3A%2F%2Favatars1.githubusercontent.com%2Fu%2F6913320%3Fs%3D400%26v%3D4',
type: '',
width: 0,
height: 0,
},
],
audios: null,
videos: null,
},
},
],
emojis: [
{
id: 'dgwyadacdbbwjc8t357h6hwsrh',
create_at: 1502389307432,
update_at: 1502389307432,
delete_at: 0,
creator_id: 'x6sdh1ok1tyd9f4dgq4ybw839a',
name: 'thanks',
},
],
files: [
{
id: 'f1oxe5rtepfs7n3zifb4sso7po',
user_id: '89ertha8xpfsumpucqppy5knao',
post_id: 'a7ebyw883trm884p1qcgt8yw4a',
create_at: 1608270920357,
update_at: 1608270920357,
delete_at: 0,
name: '4qtwrg.jpg',
extension: 'jpg',
size: 89208,
mime_type: 'image/jpeg',
width: 500,
height: 656,
has_preview_image: true,
mini_preview:
'/9j/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABAAEAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AN/T/iZp+pX15FpUmnwLbXtpJpyy2sQLw8CcBXA+bksCDnHGOaf4W+P3xIshbQ6loB8RrbK11f3FpbBFW3ZwiFGHB2kr25BIOeCPPbX4S3407T7rTdDfxFNIpDyRaw9lsB4OECHGR15yO4GK6fRPhR4sGmSnxAs8NgchNOjvDPsjz8qSHA37cDk5JPPFdlOpTdPlcVt/Ku1lrvr17b67EPnjrH8/626H/9k=',
},
],
},
},
{
id: '8fcnk3p1jt8mmkaprgajoxz115a',
create_at: 1596104683748,
update_at: 1596104683748,
edit_at: 0,
delete_at: 0,
is_pinned: false,
user_id: 'hy5sq51sebfh58ktrce5ijtcwyy',
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
root_id: '8swgtrrdiff89jnsiwiip3y1eoe',
parent_id: '',
original_id: '',
message: 'a added to the channel by j.',
type: 'system_add_to_channel',
props: {
addedUserId: 'z89qsntet7bimd3xddfu7u9ncdaxc',
addedUsername: 'a',
userId: 'hy5sdfdfq51sebfh58ktrce5ijtcwy',
username: 'j',
},
hashtags: '',
pending_post_id: '',
reply_count: 0,
last_reply_at: 0,
participants: null,
metadata: {},
},
{
id: '3y3w3a6gkbg73bnj3xund9o5ic',
create_at: 1596277483749,
update_at: 1596277483749,
edit_at: 0,
delete_at: 0,
is_pinned: false,
user_id: '44ud4m9tqwby3mphzzdwm7h31sr',
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
root_id: '8swgtrrdiff89jnsiwiip3y1eoe',
parent_id: 'ps81iqbwesfby8jayz7owg4yypo',
original_id: '',
message: 'Great work M!',
type: '',
props: {},
hashtags: '',
pending_post_id: '',
reply_count: 4,
last_reply_at: 0,
participants: null,
metadata: {},
},
];
const spyOnHandleFiles = jest.spyOn(DataOperator as any, 'handleFiles');
const spyOnHandlePostMetadata = jest.spyOn(DataOperator as any, 'handlePostMetadata');
const spyOnHandleReactions = jest.spyOn(DataOperator as any, 'handleReactions');
const spyOnHandleCustomEmojis = jest.spyOn(DataOperator as any, 'handleIsolatedEntity');
const spyOnHandlePostsInThread = jest.spyOn(DataOperator as any, 'handlePostsInThread');
const spyOnHandlePostsInChannel = jest.spyOn(DataOperator as any, 'handlePostsInChannel');
await createTestConnection({databaseName: 'post_handler', setActive: true});
// handlePosts will in turn call handlePostsInThread
await DataOperator.handlePosts({
orders: [
'8swgtrrdiff89jnsiwiip3y1eoe',
'8fcnk3p1jt8mmkaprgajoxz115a',
'3y3w3a6gkbg73bnj3xund9o5ic',
],
values: posts,
previousPostId: '',
});
expect(spyOnHandleReactions).toHaveBeenCalledTimes(1);
expect(spyOnHandleReactions).toHaveBeenCalledWith({
reactions: [
{
user_id: 'njic1w1k5inefp848jwk6oukio',
post_id: 'a7ebyw883trm884p1qcgt8yw4a',
emoji_name: 'clap',
create_at: 1608252965442,
update_at: 1608252965442,
delete_at: 0,
},
],
prepareRecordsOnly: true,
});
expect(spyOnHandleFiles).toHaveBeenCalledTimes(1);
expect(spyOnHandleFiles).toHaveBeenCalledWith({
files: [
{
id: 'f1oxe5rtepfs7n3zifb4sso7po',
user_id: '89ertha8xpfsumpucqppy5knao',
post_id: 'a7ebyw883trm884p1qcgt8yw4a',
create_at: 1608270920357,
update_at: 1608270920357,
delete_at: 0,
name: '4qtwrg.jpg',
extension: 'jpg',
size: 89208,
mime_type: 'image/jpeg',
width: 500,
height: 656,
has_preview_image: true,
mini_preview:
'/9j/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABAAEAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AN/T/iZp+pX15FpUmnwLbXtpJpyy2sQLw8CcBXA+bksCDnHGOaf4W+P3xIshbQ6loB8RrbK11f3FpbBFW3ZwiFGHB2kr25BIOeCPPbX4S3407T7rTdDfxFNIpDyRaw9lsB4OECHGR15yO4GK6fRPhR4sGmSnxAs8NgchNOjvDPsjz8qSHA37cDk5JPPFdlOpTdPlcVt/Ku1lrvr17b67EPnjrH8/626H/9k=',
},
],
prepareRecordsOnly: true,
});
expect(spyOnHandlePostMetadata).toHaveBeenCalledTimes(1);
expect(spyOnHandlePostMetadata).toHaveBeenCalledWith({
embeds: [
{
embed: [
{
type: 'opengraph',
url: 'https://github.com/mickmister/mattermost-plugin-default-theme',
data: {
type: 'object',
url: 'https://github.com/mickmister/mattermost-plugin-default-theme',
title: 'mickmister/mattermost-plugin-default-theme',
description: 'Contribute to mickmister/mattermost-plugin-default-theme development by creating an account on GitHub.',
determiner: '',
site_name: 'GitHub',
locale: '',
locales_alternate: null,
images: [
{
url: '',
secure_url: 'https://community-release.mattermost.com/api/v4/image?url=https%3A%2F%2Favatars1.githubusercontent.com%2Fu%2F6913320%3Fs%3D400%26v%3D4',
type: '',
width: 0,
height: 0,
},
],
audios: null,
videos: null,
},
},
],
postId: '8swgtrrdiff89jnsiwiip3y1eoe',
},
],
images: [
{
images: {
'https://community-release.mattermost.com/api/v4/image?url=https%3A%2F%2Favatars1.githubusercontent.com%2Fu%2F6913320%3Fs%3D400%26v%3D4': {
width: 400,
height: 400,
format: 'png',
frame_count: 0,
},
},
postId: '8swgtrrdiff89jnsiwiip3y1eoe',
},
],
prepareRecordsOnly: true,
});
expect(spyOnHandleCustomEmojis).toHaveBeenCalledTimes(1);
expect(spyOnHandleCustomEmojis).toHaveBeenCalledWith({
tableName: 'CustomEmoji',
prepareRecordsOnly: false,
values: [
{
id: 'dgwyadacdbbwjc8t357h6hwsrh',
create_at: 1502389307432,
update_at: 1502389307432,
delete_at: 0,
creator_id: 'x6sdh1ok1tyd9f4dgq4ybw839a',
name: 'thanks',
},
],
});
expect(spyOnHandlePostsInThread).toHaveBeenCalledTimes(1);
expect(spyOnHandlePostsInThread).toHaveBeenCalledWith([
{earliest: 1596032651747, post_id: '8swgtrrdiff89jnsiwiip3y1eoe'},
]);
expect(spyOnHandlePostsInChannel).toHaveBeenCalledTimes(1);
expect(spyOnHandlePostsInChannel).toHaveBeenCalledWith(posts.slice(0, 3));
});
});

View File

@@ -0,0 +1,491 @@
// 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 {isRecordDraftEqualToRaw, isRecordPostEqualToRaw} from '@database/operator/comparators';
import {
prepareDraftRecord,
prepareFileRecord,
preparePostInThreadRecord,
preparePostMetadataRecord,
preparePostRecord,
preparePostsInChannelRecord,
} from '@database/operator/prepareRecords/post';
import {getRawRecordPairs, getUniqueRawsBy, retrieveRecords} from '@database/operator/utils/general';
import {createPostsChain, sanitizePosts} from '@database/operator/utils/post';
import {Q} from '@nozbe/watermelondb';
import Model from '@nozbe/watermelondb/Model';
import {
HandleDraftArgs,
HandleFilesArgs,
HandlePostMetadataArgs,
HandlePostsArgs,
PostImage,
RawCustomEmoji,
RawEmbed,
RawFile,
RawPost,
RawPostMetadata,
RawPostsInThread,
RawReaction, RecordPair,
} from '@typings/database/database';
import Draft from '@typings/database/draft';
import {IsolatedEntities} from '@typings/database/enums';
import File from '@typings/database/file';
import Post from '@typings/database/post';
import PostMetadata from '@typings/database/post_metadata';
import PostsInChannel from '@typings/database/posts_in_channel';
import PostsInThread from '@typings/database/posts_in_thread';
import Reaction from '@typings/database/reaction';
const {
DRAFT,
FILE,
POST,
POSTS_IN_CHANNEL,
POSTS_IN_THREAD,
POST_METADATA,
} = MM_TABLES.SERVER;
export interface PostHandlerMix {
handleDraft: ({drafts, prepareRecordsOnly}: HandleDraftArgs) => Draft[] | boolean
handleFiles: ({files, prepareRecordsOnly}: HandleFilesArgs) => Promise<File[] | any[]>;
handlePostMetadata: ({embeds, images, prepareRecordsOnly}: HandlePostMetadataArgs) => Promise<any[] | PostMetadata[]>;
handlePosts: ({orders, values, previousPostId}: HandlePostsArgs) => Promise<void>;
handlePostsInChannel: (posts: RawPost[]) => Promise<void>;
handlePostsInThread: (rootPosts: RawPostsInThread[]) => Promise<void>;
}
const PostHandler = (superclass: any) => class extends superclass {
/**
* handleDraft: Handler responsible for the Create/Update operations occurring the Draft entity from the 'Server' schema
* @param {HandleDraftArgs} draftsArgs
* @param {RawDraft[]} draftsArgs.drafts
* @param {boolean} draftsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {Draft[] | boolean}
*/
handleDraft = async ({drafts, prepareRecordsOnly = true}: HandleDraftArgs) => {
if (!drafts.length) {
throw new DataOperatorException(
'An empty "drafts" array has been passed to the handleReactions method',
);
}
const rawValues = getUniqueRawsBy({raws: drafts, key: 'channel_id'});
const records = await this.handleEntityRecords({
fieldName: 'channel_id',
findMatchingRecordBy: isRecordDraftEqualToRaw,
operator: prepareDraftRecord,
prepareRecordsOnly,
rawValues,
tableName: DRAFT,
});
return prepareRecordsOnly && records?.length && records;
};
/**
* handlePosts: Handler responsible for the Create/Update operations occurring on the Post entity from the 'Server' schema
* @param {HandlePostsArgs} handlePosts
* @param {string[]} handlePosts.orders
* @param {RawPost[]} handlePosts.values
* @param {string | undefined} handlePosts.previousPostId
* @returns {Promise<void>}
*/
handlePosts = async ({orders, values, previousPostId}: HandlePostsArgs) => {
const tableName = POST;
// We rely on the order array; if it is empty, we stop processing
if (!orders.length) {
throw new DataOperatorException(
'An empty "order" array has been passed to the handlePosts method',
);
}
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: rawValues,
orders,
});
// Here we verify in our database that the postsOrdered truly need 'CREATION'
const futureEntries = await this.processInputs({
rawValues: postsOrdered,
tableName,
findMatchingRecordBy: isRecordPostEqualToRaw,
fieldName: 'id',
});
if (futureEntries.createRaws?.length) {
let batch: Model[] = [];
let files: RawFile[] = [];
const postsInThread = [];
let reactions: RawReaction[] = [];
let emojis: RawCustomEmoji[] = [];
const images: { images: Dictionary<PostImage>; postId: string }[] = [];
const embeds: { embed: RawEmbed[]; postId: string }[] = [];
// We create the 'chain of posts' by linking each posts' previousId to the post before it in the order array
const linkedRawPosts: RecordPair[] = createPostsChain({
orders,
previousPostId: previousPostId || '',
rawPosts: postsOrdered,
});
const database = await this.getDatabase(tableName);
// Prepares records for batch processing onto the 'Post' entity for the server schema
const posts = (await this.prepareRecords({
createRaws: linkedRawPosts,
database,
recordOperator: preparePostRecord,
tableName,
})) as Post[];
// Appends the processed records into the final batch array
batch = batch.concat(posts);
// Starts extracting information from each post to build up for related entities' data
for (const post of postsOrdered) {
// PostInThread handler: checks for id === root_id , if so, then call PostsInThread operator
if (!post.root_id) {
postsInThread.push({
earliest: post.create_at,
post_id: post.id,
});
}
if (post?.metadata && Object.keys(post?.metadata).length > 0) {
const metadata = post.metadata;
// Extracts reaction from post's metadata
reactions = reactions.concat(metadata?.reactions ?? []);
// Extracts emojis from post's metadata
emojis = emojis.concat(metadata?.emojis ?? []);
// Extracts files from post's metadata
files = files.concat(metadata?.files ?? []);
// Extracts images and embeds from post's metadata
if (metadata?.images) {
images.push({images: metadata.images, postId: post.id});
}
if (metadata?.embeds) {
embeds.push({embed: metadata.embeds, postId: post.id});
}
}
}
if (reactions.length) {
// calls handler for Reactions
const postReactions = (await this.handleReactions({reactions, prepareRecordsOnly: true})) as Reaction[];
batch = batch.concat(postReactions);
}
if (files.length) {
// calls handler for Files
const postFiles = await this.handleFiles({files, prepareRecordsOnly: true});
batch = batch.concat(postFiles);
}
if (images.length || embeds.length) {
// calls handler for postMetadata ( embeds and images )
const postMetadata = await this.handlePostMetadata({
images,
embeds,
prepareRecordsOnly: true,
});
batch = batch.concat(postMetadata);
}
if (batch.length) {
await this.batchOperations({database, models: batch});
}
// LAST: calls handler for CustomEmojis, PostsInThread, PostsInChannel
if (emojis.length) {
await this.handleIsolatedEntity({
tableName: IsolatedEntities.CUSTOM_EMOJI,
values: emojis,
prepareRecordsOnly: false,
});
}
if (postsInThread.length) {
await this.handlePostsInThread(postsInThread);
}
if (postsOrdered.length) {
await this.handlePostsInChannel(postsOrdered);
}
}
if (postsUnordered.length) {
// Truly update those posts that have a different update_at value
await this.handleEntityRecords({
findMatchingRecordBy: isRecordPostEqualToRaw,
fieldName: 'id',
operator: preparePostRecord,
rawValues: postsUnordered,
tableName: POST,
prepareRecordsOnly: false,
});
}
};
/**
* handleFiles: Handler responsible for the Create/Update operations occurring on the File entity from the 'Server' schema
* @param {HandleFilesArgs} handleFiles
* @param {RawFile[]} handleFiles.files
* @param {boolean} handleFiles.prepareRecordsOnly
* @returns {Promise<File[] | any[]>}
*/
handleFiles = async ({files, prepareRecordsOnly}: HandleFilesArgs) => {
if (!files.length) {
return [];
}
const database = await this.getDatabase(FILE);
const postFiles = await this.prepareRecords({
createRaws: getRawRecordPairs(files),
database,
recordOperator: prepareFileRecord,
tableName: FILE,
});
if (prepareRecordsOnly) {
return postFiles;
}
if (postFiles?.length) {
await this.batchOperations({database, models: [...postFiles]});
}
return [];
};
/**
* handlePostMetadata: Handler responsible for the Create/Update operations occurring on the PostMetadata entity from the 'Server' schema
* @param {HandlePostMetadataArgs} handlePostMetadata
* @param {{embed: RawEmbed[], postId: string}[] | undefined} handlePostMetadata.embeds
* @param {{images: Dictionary<PostImage>, postId: string}[] | undefined} handlePostMetadata.images
* @param {boolean} handlePostMetadata.prepareRecordsOnly
* @returns {Promise<any[] | PostMetadata[]>}
*/
handlePostMetadata = async ({embeds, images, prepareRecordsOnly}: HandlePostMetadataArgs) => {
const metadata: RawPostMetadata[] = [];
if (images?.length) {
images.forEach((image) => {
const imageEntry = Object.entries(image.images);
metadata.push({
data: {...imageEntry?.[0]?.[1], url: imageEntry?.[0]?.[0]},
type: 'images',
postId: image.postId,
});
});
}
if (embeds?.length) {
embeds.forEach((postEmbed) => {
postEmbed.embed.forEach((embed: RawEmbed) => {
metadata.push({
data: {...embed.data},
type: embed.type,
postId: postEmbed.postId,
});
});
});
}
if (!metadata.length) {
return [];
}
const database = await this.getDatabase(POST_METADATA);
const postMetas = await this.prepareRecords({
createRaws: getRawRecordPairs(metadata),
database,
recordOperator: preparePostMetadataRecord,
tableName: POST_METADATA,
});
if (prepareRecordsOnly) {
return postMetas;
}
if (postMetas?.length) {
await this.batchOperations({database, models: [...postMetas]});
}
return [];
};
/**
* handlePostsInThread: Handler responsible for the Create/Update operations occurring on the PostsInThread entity from the 'Server' schema
* @param {RawPostsInThread[]} rootPosts
* @returns {Promise<void>}
*/
handlePostsInThread = async (rootPosts: RawPostsInThread[]) => {
if (!rootPosts.length) {
return;
}
const postIds = rootPosts.map((postThread) => postThread.post_id);
const rawPostsInThreads: RawPostsInThread[] = [];
const database = await this.getDatabase(POSTS_IN_THREAD);
// Retrieves all threads whereby their root_id can be one of the element in the postIds array
const threads = (await database.collections.
get(POST).
query(Q.where('root_id', Q.oneOf(postIds))).
fetch()) as Post[];
// The aim here is to find the last reply in that thread; hence the latest create_at value
rootPosts.forEach((rootPost) => {
const maxCreateAt: number = threads.reduce((max: number, thread: Post) => {
return thread.createAt > max ? thread.createAt : maxCreateAt;
}, 0);
// Collects all 'raw' postInThreads objects that will be sent to the operatePostsInThread function
rawPostsInThreads.push({...rootPost, latest: maxCreateAt});
});
if (rawPostsInThreads.length) {
const postInThreadRecords = (await this.prepareRecords({
createRaws: getRawRecordPairs(rawPostsInThreads),
database,
recordOperator: preparePostInThreadRecord,
tableName: POSTS_IN_THREAD,
})) as PostsInThread[];
if (postInThreadRecords?.length) {
await this.batchOperations({database, models: postInThreadRecords});
}
}
};
/**
* handlePostsInChannel: Handler responsible for the Create/Update operations occurring on the PostsInChannel entity from the 'Server' schema
* @param {RawPost[]} posts
* @returns {Promise<void>}
*/
handlePostsInChannel = async (posts: RawPost[]) => {
// At this point, the parameter 'posts' is already a chain of posts. Now, we have to figure out how to plug it
// into existing chains in the PostsInChannel table
if (!posts.length) {
return [];
}
// Sort a clone of 'posts' array by create_at
const sortedPosts = [...posts].sort((a, b) => {
return a.create_at - b.create_at;
});
// The first element (beginning of chain)
const tipOfChain: RawPost = sortedPosts[0];
// Channel Id for this chain of posts
const channelId = tipOfChain.channel_id;
// Find smallest 'create_at' value in chain
const earliest = tipOfChain.create_at;
// Find highest 'create_at' value in chain; -1 means we are dealing with one item in the posts array
const latest = sortedPosts[sortedPosts.length - 1].create_at;
const database = await this.getDatabase(POSTS_IN_CHANNEL);
// Find the records in the PostsInChannel table that have a matching channel_id
// const chunks = (await database.collections.get(POSTS_IN_CHANNEL).query(Q.where('channel_id', channelId)).fetch()) as PostsInChannel[];
const chunks = (await retrieveRecords({
database,
tableName: POSTS_IN_CHANNEL,
condition: Q.where('channel_id', channelId),
})) as PostsInChannel[];
const createPostsInChannelRecord = async () => {
await this.executeInDatabase({
createRaws: [{record: undefined, raw: {channel_id: channelId, earliest, latest}}],
tableName: POSTS_IN_CHANNEL,
recordOperator: preparePostsInChannelRecord,
});
};
// chunk length 0; then it's a new chunk to be added to the PostsInChannel table
if (chunks.length === 0) {
await createPostsInChannelRecord();
return [];
}
// Sort chunks (in-place) by earliest field ( oldest to newest )
chunks.sort((a, b) => {
return a.earliest - b.earliest;
});
let found = false;
let targetChunk: PostsInChannel;
for (const chunk of chunks) {
// find if we should plug the chain before
if (earliest < chunk.earliest) {
found = true;
targetChunk = chunk;
}
if (found) {
break;
}
}
if (found) {
// We have a potential chunk to plug nearby
const potentialPosts = (await retrieveRecords({
database,
tableName: POST,
condition: Q.where('create_at', earliest),
})) as Post[];
if (potentialPosts?.length > 0) {
const targetPost = potentialPosts[0];
// now we decide if we need to operate on the targetChunk or just create a new chunk
const isChainable = tipOfChain.prev_post_id === targetPost.previousPostId;
if (isChainable) {
// Update this chunk's data in PostsInChannel table. earliest comes from tipOfChain while latest comes from chunk
await database.action(async () => {
await targetChunk.update((postInChannel) => {
postInChannel.earliest = earliest;
});
});
} else {
await createPostsInChannelRecord();
return [];
}
}
} else {
await createPostsInChannelRecord();
return [];
}
return [];
};
};
export default PostHandler;

View File

@@ -0,0 +1,298 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {DataOperator} from '@database/operator';
import {
isRecordMyTeamEqualToRaw,
isRecordSlashCommandEqualToRaw,
isRecordTeamChannelHistoryEqualToRaw,
isRecordTeamEqualToRaw,
isRecordTeamMembershipEqualToRaw,
isRecordTeamSearchHistoryEqualToRaw,
} from '@database/operator/comparators';
import {
prepareMyTeamRecord,
prepareSlashCommandRecord,
prepareTeamChannelHistoryRecord,
prepareTeamMembershipRecord,
prepareTeamRecord,
prepareTeamSearchHistoryRecord,
} from '@database/operator/prepareRecords/team';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
jest.mock('@database/manager');
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** Operator: Team Handlers tests ***', () => {
it('=> HandleTeam: should write to TEAM entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'team_handler', setActive: true});
await DataOperator.handleTeam({
teams: [
{
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,
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'id',
rawValues: [
{
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',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamEqualToRaw,
operator: prepareTeamRecord,
});
});
it('=> HandleTeamMemberships: should write to TEAM_MEMBERSHIP entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'team_handler', setActive: true});
await DataOperator.handleTeamMemberships({
teamMemberships: [
{
team_id: 'a',
user_id: 'ab',
roles: '3ngdqe1e7tfcbmam4qgnxp91bw',
delete_at: 0,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
rawValues: [
{
team_id: 'a',
user_id: 'ab',
roles: '3ngdqe1e7tfcbmam4qgnxp91bw',
delete_at: 0,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
],
tableName: 'TeamMembership',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamMembershipEqualToRaw,
operator: prepareTeamMembershipRecord,
});
});
it('=> HandleMyTeam: should write to MY_TEAM entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'team_handler', setActive: true});
await DataOperator.handleMyTeam({
myTeams: [
{
team_id: 'teamA',
roles: 'roleA, roleB, roleC',
is_unread: true,
mentions_count: 3,
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'team_id',
rawValues: [
{
team_id: 'teamA',
roles: 'roleA, roleB, roleC',
is_unread: true,
mentions_count: 3,
},
],
tableName: 'MyTeam',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordMyTeamEqualToRaw,
operator: prepareMyTeamRecord,
});
});
it('=> HandleTeamChannelHistory: should write to TEAM_CHANNEL_HISTORY entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'team_handler', setActive: true});
await DataOperator.handleTeamChannelHistory({
teamChannelHistories: [
{
team_id: 'a',
channel_ids: ['ca', 'cb'],
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'team_id',
rawValues: [{team_id: 'a', channel_ids: ['ca', 'cb']}],
tableName: 'TeamChannelHistory',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamChannelHistoryEqualToRaw,
operator: prepareTeamChannelHistoryRecord,
});
});
it('=> HandleTeamSearchHistory: should write to TEAM_SEARCH_HISTORY entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'team_handler', setActive: true});
await DataOperator.handleTeamSearchHistory({
teamSearchHistories: [
{
team_id: 'a',
term: 'termA',
display_term: 'termA',
created_at: 1445538153952,
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'team_id',
rawValues: [
{
team_id: 'a',
term: 'termA',
display_term: 'termA',
created_at: 1445538153952,
},
],
tableName: 'TeamSearchHistory',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamSearchHistoryEqualToRaw,
operator: prepareTeamSearchHistoryRecord,
});
});
it('=> HandleSlashCommand: should write to SLASH_COMMAND entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'team_handler', setActive: true});
await DataOperator.handleSlashCommand({
slashCommands: [
{
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',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'id',
rawValues: [
{
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',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordSlashCommandEqualToRaw,
operator: prepareSlashCommandRecord,
});
});
});

View File

@@ -0,0 +1,230 @@
// 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 {
isRecordMyTeamEqualToRaw,
isRecordSlashCommandEqualToRaw,
isRecordTeamChannelHistoryEqualToRaw,
isRecordTeamEqualToRaw,
isRecordTeamMembershipEqualToRaw,
isRecordTeamSearchHistoryEqualToRaw,
} from '@database/operator/comparators';
import {
prepareMyTeamRecord,
prepareSlashCommandRecord,
prepareTeamChannelHistoryRecord,
prepareTeamMembershipRecord,
prepareTeamRecord,
prepareTeamSearchHistoryRecord,
} from '@database/operator/prepareRecords/team';
import {getUniqueRawsBy} from '@database/operator/utils/general';
import {
HandleMyTeamArgs,
HandleSlashCommandArgs,
HandleTeamArgs,
HandleTeamChannelHistoryArgs,
HandleTeamMembershipArgs,
HandleTeamSearchHistoryArgs,
} from '@typings/database/database';
import MyTeam from '@typings/database/my_team';
import SlashCommand from '@typings/database/slash_command';
import Team from '@typings/database/team';
import TeamChannelHistory from '@typings/database/team_channel_history';
import TeamMembership from '@typings/database/team_membership';
const {
MY_TEAM,
SLASH_COMMAND,
TEAM,
TEAM_CHANNEL_HISTORY,
TEAM_MEMBERSHIP,
TEAM_SEARCH_HISTORY,
} = MM_TABLES.SERVER;
export interface TeamHandlerMix {
handleTeamMemberships : ({teamMemberships, prepareRecordsOnly}: HandleTeamMembershipArgs) => TeamMembership[] | boolean,
handleTeam: ({teams, prepareRecordsOnly}: HandleTeamArgs) => Team[] | boolean
handleTeamChannelHistory : ({teamChannelHistories, prepareRecordsOnly}: HandleTeamChannelHistoryArgs) => TeamChannelHistory[]| boolean,
handleSlashCommand : ({slashCommands, prepareRecordsOnly} : HandleSlashCommandArgs) => SlashCommand[]| boolean,
handleMyTeam : ({myTeams, prepareRecordsOnly}: HandleMyTeamArgs) => MyTeam[]| boolean
}
const TeamHandler = (superclass: any) => class extends superclass {
/**
* handleTeamMemberships: Handler responsible for the Create/Update operations occurring on the TEAM_MEMBERSHIP entity from the 'Server' schema
* @param {HandleTeamMembershipArgs} teamMembershipsArgs
* @param {RawTeamMembership[]} teamMembershipsArgs.teamMemberships
* @param {boolean} teamMembershipsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {TeamMembership[] | boolean}
*/
handleTeamMemberships = async ({teamMemberships, prepareRecordsOnly = true}: HandleTeamMembershipArgs) => {
if (!teamMemberships.length) {
throw new DataOperatorException(
'An empty "teamMemberships" array has been passed to the handleTeamMemberships method',
);
}
const rawValues = getUniqueRawsBy({raws: teamMemberships, key: 'team_id'});
const records = await this.handleEntityRecords({
fieldName: 'user_id',
findMatchingRecordBy: isRecordTeamMembershipEqualToRaw,
operator: prepareTeamMembershipRecord,
rawValues,
tableName: TEAM_MEMBERSHIP,
prepareRecordsOnly,
});
return prepareRecordsOnly && records?.length && records;
};
/**
* handleTeam: Handler responsible for the Create/Update operations occurring on the TEAM entity from the 'Server' schema
* @param {HandleTeamArgs} teamsArgs
* @param {RawTeam[]} teamsArgs.teams
* @param {boolean} teamsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {Team[] | boolean}
*/
handleTeam = async ({teams, prepareRecordsOnly = true}: HandleTeamArgs) => {
if (!teams.length) {
throw new DataOperatorException(
'An empty "teams" array has been passed to the handleTeam method',
);
}
const rawValues = getUniqueRawsBy({raws: teams, key: 'id'});
const records = await this.handleEntityRecords({
fieldName: 'id',
findMatchingRecordBy: isRecordTeamEqualToRaw,
operator: prepareTeamRecord,
prepareRecordsOnly,
rawValues,
tableName: TEAM,
});
return prepareRecordsOnly && records?.length && records;
};
/**
* handleTeamChannelHistory: Handler responsible for the Create/Update operations occurring on the TEAM_CHANNEL_HISTORY entity from the 'Server' schema
* @param {HandleTeamChannelHistoryArgs} teamChannelHistoriesArgs
* @param {RawTeamChannelHistory[]} teamChannelHistoriesArgs.teamChannelHistories
* @param {boolean} teamChannelHistoriesArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {TeamChannelHistory[]| boolean}
*/
handleTeamChannelHistory = async ({teamChannelHistories, prepareRecordsOnly = true}: HandleTeamChannelHistoryArgs) => {
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'});
const records = await this.handleEntityRecords({
fieldName: 'team_id',
findMatchingRecordBy: isRecordTeamChannelHistoryEqualToRaw,
operator: prepareTeamChannelHistoryRecord,
prepareRecordsOnly,
rawValues,
tableName: TEAM_CHANNEL_HISTORY,
});
return prepareRecordsOnly && records?.length && records;
};
/**
* handleTeamSearchHistory: Handler responsible for the Create/Update operations occurring on the TEAM_SEARCH_HISTORY entity from the 'Server' schema
* @param {HandleTeamSearchHistoryArgs} teamSearchHistoriesArgs
* @param {RawTeamSearchHistory[]} teamSearchHistoriesArgs.teamSearchHistories
* @param {boolean} teamSearchHistoriesArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {TeamSearchHistory[]| boolean}
*/
handleTeamSearchHistory = async ({teamSearchHistories, prepareRecordsOnly = true}: HandleTeamSearchHistoryArgs) => {
if (!teamSearchHistories.length) {
throw new DataOperatorException(
'An empty "teamSearchHistories" array has been passed to the handleTeamSearchHistory method',
);
}
const rawValues = getUniqueRawsBy({raws: teamSearchHistories, key: 'term'});
const records = await this.handleEntityRecords({
fieldName: 'team_id',
findMatchingRecordBy: isRecordTeamSearchHistoryEqualToRaw,
operator: prepareTeamSearchHistoryRecord,
prepareRecordsOnly,
rawValues,
tableName: TEAM_SEARCH_HISTORY,
});
return prepareRecordsOnly && records?.length && records;
};
/**
* handleSlashCommand: Handler responsible for the Create/Update operations occurring on the SLASH_COMMAND entity from the 'Server' schema
* @param {HandleSlashCommandArgs} slashCommandsArgs
* @param {RawSlashCommand[]} slashCommandsArgs.slashCommands
* @param {boolean} slashCommandsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {SlashCommand[]| boolean}
*/
handleSlashCommand = async ({slashCommands, prepareRecordsOnly = true} : HandleSlashCommandArgs) => {
if (!slashCommands.length) {
throw new DataOperatorException(
'An empty "slashCommands" array has been passed to the handleSlashCommand method',
);
}
const rawValues = getUniqueRawsBy({raws: slashCommands, key: 'id'});
const records = await this.handleEntityRecords({
fieldName: 'id',
findMatchingRecordBy: isRecordSlashCommandEqualToRaw,
operator: prepareSlashCommandRecord,
prepareRecordsOnly,
rawValues,
tableName: SLASH_COMMAND,
});
return prepareRecordsOnly && records?.length && records;
};
/**
* handleMyTeam: Handler responsible for the Create/Update operations occurring on the MY_TEAM entity from the 'Server' schema
* @param {HandleMyTeamArgs} myTeamsArgs
* @param {RawMyTeam[]} myTeamsArgs.myTeams
* @param {boolean} myTeamsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {MyTeam[]| boolean}
*/
handleMyTeam = async ({myTeams, prepareRecordsOnly = true}: HandleMyTeamArgs) => {
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'});
const records = await this.handleEntityRecords({
fieldName: 'team_id',
findMatchingRecordBy: isRecordMyTeamEqualToRaw,
operator: prepareMyTeamRecord,
prepareRecordsOnly,
rawValues,
tableName: MY_TEAM,
});
return prepareRecordsOnly && records?.length && records;
};
};
export default TeamHandler;

View File

@@ -0,0 +1,332 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {DataOperator} from '@database/operator';
import {
isRecordChannelMembershipEqualToRaw,
isRecordPreferenceEqualToRaw,
isRecordUserEqualToRaw,
} from '@database/operator/comparators';
import {
prepareChannelMembershipRecord,
preparePreferenceRecord,
prepareUserRecord,
} from '@database/operator/prepareRecords/user';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
jest.mock('@database/manager');
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** Operator: User Handlers tests ***', () => {
it('=> HandleReactions: should write to both Reactions and CustomEmoji entities', async () => {
expect.assertions(2);
await createTestConnection({databaseName: 'user_handler', setActive: true});
const spyOnPrepareRecords = jest.spyOn(DataOperator as any, 'prepareRecords');
const spyOnBatchOperation = jest.spyOn(DataOperator as any, 'batchOperations');
await DataOperator.handleReactions({
reactions: [
{
create_at: 1608263728086,
delete_at: 0,
emoji_name: 'p4p1',
post_id: '4r9jmr7eqt8dxq3f9woypzurry',
update_at: 1608263728077,
user_id: 'ooumoqgq3bfiijzwbn8badznwc',
},
],
prepareRecordsOnly: false,
});
// Called twice: Once for Reaction record and once for CustomEmoji record
expect(spyOnPrepareRecords).toHaveBeenCalledTimes(2);
// Only one batch operation for both entities
expect(spyOnBatchOperation).toHaveBeenCalledTimes(1);
});
it('=> HandleUsers: should write to User entity', async () => {
expect.assertions(2);
const users = [
{
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,
},
},
];
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'user_handler', setActive: true});
await DataOperator.handleUsers({users, prepareRecordsOnly: false});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'id',
rawValues: [
{
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',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordUserEqualToRaw,
operator: prepareUserRecord,
});
});
it('=> HandlePreferences: should write to PREFERENCE entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'user_handler', setActive: true});
await DataOperator.handlePreferences({
preferences: [
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'group_channel_show',
name: 'qj91hepgjfn6xr4acm5xzd8zoc',
value: 'true',
},
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'notifications',
name: 'email_interval',
value: '30',
},
{
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"}',
},
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'tutorial_step',
name: '9ciscaqbrpd6d8s68k76xb9bte',
value: '2',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
rawValues: [
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'group_channel_show',
name: 'qj91hepgjfn6xr4acm5xzd8zoc',
value: 'true',
},
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'notifications',
name: 'email_interval',
value: '30',
},
{
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"}',
},
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'tutorial_step',
name: '9ciscaqbrpd6d8s68k76xb9bte',
value: '2',
},
],
tableName: 'Preference',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordPreferenceEqualToRaw,
operator: preparePreferenceRecord,
});
});
it('=> HandleChannelMembership: should write to CHANNEL_MEMBERSHIP entity', async () => {
expect.assertions(2);
const channelMemberships = [
{
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: '',
},
{
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: '',
},
];
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'user_handler', setActive: true});
await DataOperator.handleChannelMembership({
channelMemberships,
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
rawValues: [
{
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: '',
},
{
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',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordChannelMembershipEqualToRaw,
operator: prepareChannelMembershipRecord,
});
});
});

View File

@@ -0,0 +1,210 @@
// 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 {
isRecordChannelMembershipEqualToRaw,
isRecordPreferenceEqualToRaw,
isRecordUserEqualToRaw,
} from '@database/operator/comparators';
import {prepareCustomEmojiRecord} from '@database/operator/prepareRecords/general';
import {
prepareChannelMembershipRecord,
preparePreferenceRecord,
prepareReactionRecord,
prepareUserRecord,
} from '@database/operator/prepareRecords/user';
import {getRawRecordPairs, getUniqueRawsBy} from '@database/operator/utils/general';
import {sanitizeReactions} from '@database/operator/utils/reaction';
import Model from '@nozbe/watermelondb/Model';
import ChannelMembership from '@typings/database/channel_membership';
import CustomEmoji from '@typings/database/custom_emoji';
import {
HandleChannelMembershipArgs,
HandlePreferencesArgs,
HandleReactionsArgs,
HandleUsersArgs,
RawReaction,
} from '@typings/database/database';
import Preference from '@typings/database/preference';
import Reaction from '@typings/database/reaction';
import User from '@typings/database/user';
const {
CHANNEL_MEMBERSHIP,
CUSTOM_EMOJI,
PREFERENCE,
REACTION,
USER,
} = MM_TABLES.SERVER;
export interface UserHandlerMix {
handleChannelMembership : ({channelMemberships, prepareRecordsOnly}: HandleChannelMembershipArgs) => ChannelMembership[] | boolean,
handlePreferences : ({preferences, prepareRecordsOnly}: HandlePreferencesArgs) => Preference[] | boolean,
handleReactions : ({reactions, prepareRecordsOnly}: HandleReactionsArgs) => boolean | (Reaction | CustomEmoji)[],
handleUsers : ({users, prepareRecordsOnly}: HandleUsersArgs) => User[] | boolean
}
const UserHandler = (superclass: any) => class extends superclass {
/**
* handleChannelMembership: Handler responsible for the Create/Update operations occurring on the CHANNEL_MEMBERSHIP entity from the 'Server' schema
* @param {HandleChannelMembershipArgs} channelMembershipsArgs
* @param {RawChannelMembership[]} channelMembershipsArgs.channelMemberships
* @param {boolean} channelMembershipsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {ChannelMembership[] | boolean}
*/
handleChannelMembership = async ({channelMemberships, prepareRecordsOnly = true}: HandleChannelMembershipArgs) => {
if (!channelMemberships.length) {
throw new DataOperatorException(
'An empty "channelMemberships" array has been passed to the handleChannelMembership method',
);
}
const rawValues = getUniqueRawsBy({
raws: channelMemberships,
key: 'channel_id',
});
const records = await this.handleEntityRecords({
fieldName: 'user_id',
findMatchingRecordBy: isRecordChannelMembershipEqualToRaw,
operator: prepareChannelMembershipRecord,
prepareRecordsOnly,
rawValues,
tableName: CHANNEL_MEMBERSHIP,
});
return prepareRecordsOnly && records?.length && records;
};
/**
* handlePreferences: Handler responsible for the Create/Update operations occurring on the PREFERENCE entity from the 'Server' schema
* @param {HandlePreferencesArgs} preferencesArgs
* @param {RawPreference[]} preferencesArgs.preferences
* @param {boolean} preferencesArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {Preference[] | boolean}
*/
handlePreferences = async ({preferences, prepareRecordsOnly = true}: HandlePreferencesArgs) => {
if (!preferences.length) {
throw new DataOperatorException(
'An empty "preferences" array has been passed to the handlePreferences method',
);
}
const rawValues = getUniqueRawsBy({raws: preferences, key: 'name'});
const records = await this.handleEntityRecords({
fieldName: 'user_id',
findMatchingRecordBy: isRecordPreferenceEqualToRaw,
operator: preparePreferenceRecord,
prepareRecordsOnly,
rawValues,
tableName: PREFERENCE,
});
return prepareRecordsOnly && records?.length && records;
};
/**
* handleReactions: Handler responsible for the Create/Update operations occurring on the Reaction entity from the 'Server' schema
* @param {HandleReactionsArgs} handleReactions
* @param {RawReaction[]} handleReactions.reactions
* @param {boolean} handleReactions.prepareRecordsOnly
* @throws DataOperatorException
* @returns {boolean | (Reaction | CustomEmoji)[]}
*/
handleReactions = async ({reactions, prepareRecordsOnly}: HandleReactionsArgs) => {
if (!reactions.length) {
throw new DataOperatorException(
'An empty "reactions" array has been passed to the handleReactions method',
);
}
const rawValues = getUniqueRawsBy({raws: reactions, key: 'emoji_name'}) as RawReaction[];
const database = await this.getDatabase(REACTION);
const {
createEmojis,
createReactions,
deleteReactions,
} = await sanitizeReactions({
database,
post_id: reactions[0].post_id,
rawReactions: rawValues,
});
let batchRecords: Model[] = [];
if (createReactions.length) {
// Prepares record for model Reactions
const reactionsRecords = (await this.prepareRecords({
createRaws: createReactions,
database,
recordOperator: prepareReactionRecord,
tableName: REACTION,
})) as Reaction[];
batchRecords = batchRecords.concat(reactionsRecords);
}
if (createEmojis.length) {
// Prepares records for model CustomEmoji
const emojiRecords = (await this.prepareRecords({
createRaws: getRawRecordPairs(createEmojis),
database,
recordOperator: prepareCustomEmojiRecord,
tableName: CUSTOM_EMOJI,
})) as CustomEmoji[];
batchRecords = batchRecords.concat(emojiRecords);
}
batchRecords = batchRecords.concat(deleteReactions);
if (prepareRecordsOnly) {
return batchRecords;
}
if (batchRecords?.length) {
await this.batchOperations({
database,
models: batchRecords,
});
}
return false;
};
/**
* handleUsers: Handler responsible for the Create/Update operations occurring on the User entity from the 'Server' schema
* @param {HandleUsersArgs} usersArgs
* @param {RawUser[]} usersArgs.users
* @param {boolean} usersArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {User[] | boolean}
*/
handleUsers = async ({users, prepareRecordsOnly = true}: HandleUsersArgs) => {
if (!users.length) {
throw new DataOperatorException(
'An empty "users" array has been passed to the handleUsers method',
);
}
const rawValues = getUniqueRawsBy({raws: users, key: 'id'});
const records = await this.handleEntityRecords({
fieldName: 'id',
findMatchingRecordBy: isRecordUserEqualToRaw,
operator: prepareUserRecord,
rawValues,
tableName: USER,
prepareRecordsOnly,
});
return prepareRecordsOnly && records?.length && records;
};
};
export default UserHandler;

View File

@@ -0,0 +1,24 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import BaseHandler, {BaseHandlerMix} from '@database/operator/handlers/base_handler';
import ChannelHandler, {ChannelHandlerMix} from '@database/operator/handlers/channel';
import GroupHandler, {GroupHandlerMix} from '@database/operator/handlers/group';
import PostHandler, {PostHandlerMix} from '@database/operator/handlers/post';
import TeamHandler, {TeamHandlerMix} from '@database/operator/handlers/team';
import UserHandler, {UserHandlerMix} from '@database/operator/handlers/user';
import mix from '@utils/mix';
interface Operator extends BaseHandlerMix, PostHandlerMix, UserHandlerMix, GroupHandlerMix, ChannelHandlerMix, TeamHandlerMix {}
class Operator extends mix(BaseHandler).with(
PostHandler,
UserHandler,
GroupHandler,
ChannelHandler,
TeamHandler,
) {}
const DataOperator = new Operator();
export {DataOperator, Operator};

View File

@@ -0,0 +1,133 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
prepareChannelInfoRecord,
prepareChannelRecord,
prepareMyChannelRecord,
prepareMyChannelSettingsRecord,
} from '@database/operator/prepareRecords/channel';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
describe('*** CHANNEL Prepare Records Test ***', () => {
it('=> prepareChannelRecord: should return an array of type Channel', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'channel_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareChannelRecord({
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('=> prepareMyChannelSettingsRecord: should return an array of type MyChannelSettings', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'channel_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareMyChannelSettingsRecord({
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('=> prepareChannelInfoRecord: should return an array of type ChannelInfo', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'channel_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareChannelInfoRecord({
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('=> prepareMyChannelRecord: should return an array of type MyChannel', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'channel_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareMyChannelRecord({
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');
});
});

View File

@@ -0,0 +1,148 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/prepareRecords/index';
import Channel from '@typings/database/channel';
import ChannelInfo from '@typings/database/channel_info';
import {
DataFactoryArgs,
RawChannel,
RawChannelInfo,
RawMyChannel,
RawMyChannelSettings,
} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
import MyChannel from '@typings/database/my_channel';
import MyChannelSettings from '@typings/database/my_channel_settings';
const {
CHANNEL,
CHANNEL_INFO,
MY_CHANNEL,
MY_CHANNEL_SETTINGS,
} = MM_TABLES.SERVER;
/**
* prepareChannelRecord: Prepares record of entity 'CHANNEL' from the SERVER database for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareChannelRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawChannel;
const record = value.record as Channel;
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 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 prepareBaseRecord({
action,
database,
tableName: CHANNEL,
value,
generator,
});
};
/**
* prepareMyChannelSettingsRecord: Prepares record of entity 'MY_CHANNEL_SETTINGS' from the SERVER database for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareMyChannelSettingsRecord = ({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 prepareBaseRecord({
action,
database,
tableName: MY_CHANNEL_SETTINGS,
value,
generator,
});
};
/**
* prepareChannelInfoRecord: Prepares record of entity 'CHANNEL_INFO' from the SERVER database for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareChannelInfoRecord = ({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 prepareBaseRecord({
action,
database,
tableName: CHANNEL_INFO,
value,
generator,
});
};
/**
* prepareMyChannelRecord: Prepares record of entity 'MY_CHANNEL' from the SERVER database for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareMyChannelRecord = ({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 prepareBaseRecord({
action,
database,
tableName: MY_CHANNEL,
value,
generator,
});
};

View File

@@ -0,0 +1,177 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
prepareAppRecord,
prepareCustomEmojiRecord,
prepareGlobalRecord,
prepareRoleRecord,
prepareServersRecord,
prepareSystemRecord,
prepareTermsOfServiceRecord,
} from '@database/operator/prepareRecords/general';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import DatabaseManager from '@database/manager';
import {OperationType} from '@typings/database/enums';
describe('*** Isolated Prepare Records Test ***', () => {
it('=> prepareAppRecord: should return an array of type App', async () => {
expect.assertions(3);
const database = await DatabaseManager.getDefaultDatabase();
expect(database).toBeTruthy();
const preparedRecords = await prepareAppRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
build_number: 'build-7',
created_at: 1,
version_number: 'v-1',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('App');
});
it('=> prepareGlobalRecord: should return an array of type Global', async () => {
expect.assertions(3);
const database = await DatabaseManager.getDefaultDatabase();
expect(database).toBeTruthy();
const preparedRecords = await prepareGlobalRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {name: 'g-n1', value: 'g-v1'},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Global');
});
it('=> prepareServersRecord: should return an array of type Servers', async () => {
expect.assertions(3);
const database = await DatabaseManager.getDefaultDatabase();
expect(database).toBeTruthy();
const preparedRecords = await prepareServersRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
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).toBe('Servers');
});
it('=> prepareRoleRecord: should return an array of type Role', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'isolated_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareRoleRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'role-1',
name: 'role-name-1',
permissions: [],
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Role');
});
it('=> prepareSystemRecord: should return an array of type System', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'isolated_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareSystemRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {id: 'system-1', name: 'system-name-1', value: 'system'},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('System');
});
it('=> prepareTermsOfServiceRecord: should return an array of type TermsOfService', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'isolated_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareTermsOfServiceRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'tos-1',
accepted_at: 1,
create_at: 1613667352029,
user_id: 'user1613667352029',
text: '',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('TermsOfService');
});
it('=> prepareCustomEmojiRecord: should return an array of type CustomEmoji', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'isolated_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareCustomEmojiRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'i',
create_at: 1580913641769,
update_at: 1580913641769,
delete_at: 0,
creator_id: '4cprpki7ri81mbx8efixcsb8jo',
name: 'boomI',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('CustomEmoji');
});
});

View File

@@ -0,0 +1,227 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/prepareRecords/index';
import App from '@typings/database/app';
import CustomEmoji from '@typings/database/custom_emoji';
import {
DataFactoryArgs,
RawApp,
RawCustomEmoji,
RawGlobal,
RawRole,
RawServers,
RawSystem,
RawTermsOfService,
} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
import Global from '@typings/database/global';
import Role from '@typings/database/role';
import Servers from '@typings/database/servers';
import System from '@typings/database/system';
import TermsOfService from '@typings/database/terms_of_service';
const {APP, GLOBAL, SERVERS} = MM_TABLES.DEFAULT;
const {
CUSTOM_EMOJI,
ROLE,
SYSTEM,
TERMS_OF_SERVICE,
} = MM_TABLES.SERVER;
/**
* prepareAppRecord: Prepares record of entity 'App' from the DEFAULT database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareAppRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawApp;
const record = value.record as App;
const isCreateAction = action === OperationType.CREATE;
const generator = (app: App) => {
app._raw.id = isCreateAction ? app.id : record.id;
app.buildNumber = raw?.build_number;
app.createdAt = raw?.created_at;
app.versionNumber = raw?.version_number;
};
return prepareBaseRecord({
action,
database,
generator,
tableName: APP,
value,
});
};
/**
* prepareGlobalRecord: Prepares record of entity 'Global' from the DEFAULT database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareGlobalRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawGlobal;
const record = value.record as Global;
const isCreateAction = action === OperationType.CREATE;
const generator = (global: Global) => {
global._raw.id = isCreateAction ? global.id : record.id;
global.name = raw?.name;
global.value = raw?.value;
};
return prepareBaseRecord({
action,
database,
generator,
tableName: GLOBAL,
value,
});
};
/**
* prepareServersRecord: Prepares record of entity 'Servers' from the DEFAULT database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareServersRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawServers;
const record = value.record as Servers;
const isCreateAction = action === OperationType.CREATE;
const generator = (servers: Servers) => {
servers._raw.id = isCreateAction ? servers.id : record.id;
servers.dbPath = raw?.db_path;
servers.displayName = raw?.display_name;
servers.mentionCount = raw?.mention_count;
servers.unreadCount = raw?.unread_count;
servers.url = raw?.url;
};
return prepareBaseRecord({
action,
database,
tableName: SERVERS,
value,
generator,
});
};
/**
* prepareCustomEmojiRecord: Prepares record of entity 'CustomEmoji' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareCustomEmojiRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawCustomEmoji;
const record = value.record as CustomEmoji;
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 generator = (emoji: CustomEmoji) => {
emoji._raw.id = isCreateAction ? (raw?.id ?? emoji.id) : record.id;
emoji.name = raw.name;
};
return prepareBaseRecord({
action,
database,
tableName: CUSTOM_EMOJI,
value,
generator,
});
};
/**
* prepareRoleRecord: Prepares record of entity 'Role' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareRoleRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawRole;
const record = value.record as Role;
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 generator = (role: Role) => {
role._raw.id = isCreateAction ? (raw?.id ?? role.id) : record.id;
role.name = raw?.name;
role.permissions = raw?.permissions;
};
return prepareBaseRecord({
action,
database,
tableName: ROLE,
value,
generator,
});
};
/**
* prepareSystemRecord: Prepares record of entity 'System' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareSystemRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawSystem;
const record = value.record as System;
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 generator = (system: System) => {
system._raw.id = isCreateAction ? (raw?.id ?? system.id) : record.id;
system.name = raw?.name;
system.value = raw?.value;
};
return prepareBaseRecord({
action,
database,
tableName: SYSTEM,
value,
generator,
});
};
/**
* prepareTermsOfServiceRecord: Prepares record of entity 'TermsOfService' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareTermsOfServiceRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawTermsOfService;
const record = value.record as TermsOfService;
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 generator = (tos: TermsOfService) => {
tos._raw.id = isCreateAction ? (raw?.id ?? tos.id) : record.id;
tos.acceptedAt = raw?.accepted_at;
};
return prepareBaseRecord({
action,
database,
tableName: TERMS_OF_SERVICE,
value,
generator,
});
};

View File

@@ -0,0 +1,124 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
prepareGroupMembershipRecord,
prepareGroupRecord,
prepareGroupsInChannelRecord,
prepareGroupsInTeamRecord,
} from '@database/operator/prepareRecords/group';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
describe('*** GROUP Prepare Records Test ***', () => {
it('=> prepareGroupRecord: should return an array of type Group', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'group_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareGroupRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
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,
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Group');
});
it('=> prepareGroupsInTeamRecord: should return an array of type GroupsInTeam', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'group_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareGroupsInTeamRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
team_id: 'team_89',
team_display_name: '',
team_type: '',
group_id: 'group_id89',
auto_add: true,
create_at: 0,
delete_at: 0,
update_at: 0,
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('GroupsInTeam');
});
it('=> prepareGroupsInChannelRecord: should return an array of type GroupsInChannel', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'group_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareGroupsInChannelRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
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,
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('GroupsInChannel');
});
it('=> prepareGroupMembershipRecord: should return an array of type GroupMembership', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'group_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareGroupMembershipRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
user_id: 'u4cprpki7ri81mbx8efixcsb8jo',
group_id: 'g4cprpki7ri81mbx8efixcsb8jo',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('GroupMembership');
});
});

View File

@@ -0,0 +1,136 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/prepareRecords/index';
import {
DataFactoryArgs,
RawGroup,
RawGroupMembership,
RawGroupsInChannel,
RawGroupsInTeam,
} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
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';
const {
GROUP,
GROUPS_IN_CHANNEL,
GROUPS_IN_TEAM,
GROUP_MEMBERSHIP,
} = MM_TABLES.SERVER;
/**
* prepareGroupMembershipRecord: Prepares record of entity 'GROUP_MEMBERSHIP' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareGroupMembershipRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawGroupMembership;
const record = value.record as GroupMembership;
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 generator = (groupMember: GroupMembership) => {
groupMember._raw.id = isCreateAction ? (raw?.id ?? groupMember.id) : record.id;
groupMember.groupId = raw.group_id;
groupMember.userId = raw.user_id;
};
return prepareBaseRecord({
action,
database,
tableName: GROUP_MEMBERSHIP,
value,
generator,
});
};
/**
* prepareGroupRecord: Prepares record of entity 'GROUP' from the SERVER database for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareGroupRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawGroup;
const record = value.record as Group;
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 generator = (group: Group) => {
group._raw.id = isCreateAction ? (raw?.id ?? group.id) : record.id;
group.name = raw.name;
group.displayName = raw.display_name;
};
return prepareBaseRecord({
action,
database,
tableName: GROUP,
value,
generator,
});
};
/**
* prepareGroupsInTeamRecord: Prepares record of entity 'GROUPS_IN_TEAM' from the SERVER database for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareGroupsInTeamRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawGroupsInTeam;
const record = value.record as GroupsInTeam;
const isCreateAction = action === OperationType.CREATE;
const generator = (groupsInTeam: GroupsInTeam) => {
groupsInTeam._raw.id = isCreateAction ? groupsInTeam.id : record.id;
groupsInTeam.teamId = raw.team_id;
groupsInTeam.groupId = raw.group_id;
};
return prepareBaseRecord({
action,
database,
tableName: GROUPS_IN_TEAM,
value,
generator,
});
};
/**
* prepareGroupsInChannelRecord: Prepares record of entity 'GROUPS_IN_CHANNEL' from the SERVER database for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareGroupsInChannelRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawGroupsInChannel;
const record = value.record as GroupsInChannel;
const isCreateAction = action === OperationType.CREATE;
const generator = (groupsInChannel: GroupsInChannel) => {
groupsInChannel._raw.id = isCreateAction ? groupsInChannel.id : record.id;
groupsInChannel.channelId = raw.channel_id;
groupsInChannel.groupId = raw.group_id;
groupsInChannel.memberCount = raw.member_count;
groupsInChannel.timezoneCount = raw.timezone_count;
};
return prepareBaseRecord({
action,
database,
tableName: GROUPS_IN_CHANNEL,
value,
generator,
});
};

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Model from '@nozbe/watermelondb/Model';
import {DataFactoryArgs} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
/**
* prepareBaseRecord: 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
*
* @param {DataFactoryArgs} operatorBase
* @param {Database} operatorBase.database
* @param {string} operatorBase.tableName
* @param {RecordPair} operatorBase.value
* @param {((DataFactoryArgs) => void)} operatorBase.generator
* @returns {Promise<Model>}
*/
export const prepareBaseRecord = async ({
action,
database,
tableName,
value,
generator,
}: DataFactoryArgs): Promise<Model> => {
if (action === OperationType.UPDATE) {
const record = value.record as Model;
return record.prepareUpdate(() => generator!(record));
}
return database.collections.get(tableName!).prepareCreate(generator);
};

View File

@@ -0,0 +1,187 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
prepareDraftRecord,
prepareFileRecord,
preparePostInThreadRecord,
preparePostMetadataRecord,
preparePostRecord,
preparePostsInChannelRecord,
} from '@database/operator/prepareRecords/post';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** POST Prepare Records Test ***', () => {
it('=> preparePostRecord: should return an array of type Post', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'post_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await preparePostRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: '8swgtrrdiff89jnsiwiip3y1eoe',
create_at: 1596032651748,
update_at: 1596032651748,
edit_at: 0,
delete_at: 0,
is_pinned: false,
user_id: 'q3mzxua9zjfczqakxdkowc6u6yy',
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
root_id: 'ps81iqbesfby8jayz7owg4yypoo',
parent_id: 'ps81iqbddesfby8jayz7owg4yypoo',
original_id: '',
message: 'Testing operator post',
type: '',
props: {},
hashtags: '',
pending_post_id: '',
reply_count: 4,
last_reply_at: 0,
participants: null,
metadata: {},
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Post');
});
it('=> preparePostInThreadRecord: should return an array of type PostsInThread', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'post_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await preparePostInThreadRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'ps81iqbddesfby8jayz7owg4yypoo',
post_id: '8swgtrrdiff89jnsiwiip3y1eoe',
earliest: 1596032651748,
latest: 1597032651748,
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe(
'PostsInThread',
);
});
it('=> prepareFileRecord: should return an array of type File', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'post_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareFileRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
post_id: 'ps81iqbddesfby8jayz7owg4yypoo',
name: 'test_file',
extension: '.jpg',
size: 1000,
create_at: 1609253011321,
delete_at: 1609253011321,
height: 20,
update_at: 1609253011321,
user_id: 'wqyby5r5pinxxdqhoaomtacdhc',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('File');
});
it('=> preparePostMetadataRecord: should return an array of type PostMetadata', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'post_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await preparePostMetadataRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'ps81i4yypoo',
data: {},
postId: 'ps81iqbddesfby8jayz7owg4yypoo',
type: 'opengraph',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('PostMetadata');
});
it('=> prepareDraftRecord: should return an array of type Draft', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'post_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareDraftRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'ps81i4yypoo',
root_id: 'ps81iqbddesfby8jayz7owg4yypoo',
message: 'draft message',
channel_id: 'channel_idp23232e',
files: [],
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Draft');
});
it('=> preparePostsInChannelRecord: should return an array of type PostsInChannel', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'post_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await preparePostsInChannelRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'ps81i4yypoo',
channel_id: 'channel_idp23232e',
earliest: 1608253011321,
latest: 1609253011321,
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe(
'PostsInChannel',
);
});
});

View File

@@ -0,0 +1,218 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/prepareRecords/index';
import {Q} from '@nozbe/watermelondb';
import {
DataFactoryArgs,
RawDraft,
RawFile,
RawPost,
RawPostMetadata,
RawPostsInChannel,
RawPostsInThread,
} from '@typings/database/database';
import Draft from '@typings/database/draft';
import {OperationType} from '@typings/database/enums';
import File from '@typings/database/file';
import Post from '@typings/database/post';
import PostMetadata from '@typings/database/post_metadata';
import PostsInChannel from '@typings/database/posts_in_channel';
import PostsInThread from '@typings/database/posts_in_thread';
const {
DRAFT,
FILE,
POST,
POSTS_IN_CHANNEL,
POSTS_IN_THREAD,
POST_METADATA,
} = MM_TABLES.SERVER;
/**
* preparePostRecord: Prepares record of entity 'Post' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const preparePostRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawPost;
const record = value.record as Post;
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 generator = (post: Post) => {
post._raw.id = isCreateAction ? (raw?.id ?? post.id) : record.id;
post.channelId = raw.channel_id;
post.createAt = raw.create_at;
post.deleteAt = raw.delete_at || raw.delete_at === 0 ? raw?.delete_at : 0;
post.editAt = raw.edit_at;
post.updateAt = raw.update_at;
post.isPinned = Boolean(raw.is_pinned);
post.message = Q.sanitizeLikeString(raw.message);
post.userId = raw.user_id;
post.originalId = raw.original_id;
post.pendingPostId = raw.pending_post_id;
post.previousPostId = raw.prev_post_id ?? '';
post.rootId = raw.root_id;
post.type = raw.type ?? '';
post.props = raw.props ?? {};
};
return prepareBaseRecord({
action,
database,
tableName: POST,
value,
generator,
});
};
/**
* preparePostInThreadRecord: Prepares record of entity 'POSTS_IN_THREAD' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const preparePostInThreadRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawPostsInThread;
const record = value.record as PostsInThread;
const isCreateAction = action === OperationType.CREATE;
const generator = (postsInThread: PostsInThread) => {
postsInThread.postId = isCreateAction ? raw.post_id : record.id;
postsInThread.earliest = raw.earliest;
postsInThread.latest = raw.latest!;
};
return prepareBaseRecord({
action,
database,
tableName: POSTS_IN_THREAD,
value,
generator,
});
};
/**
* prepareFileRecord: Prepares record of entity 'FILE' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareFileRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawFile;
const record = value.record as File;
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 generator = (file: File) => {
file._raw.id = isCreateAction ? (raw?.id ?? file.id) : record.id;
file.postId = raw.post_id;
file.name = raw.name;
file.extension = raw.extension;
file.size = raw.size;
file.mimeType = raw?.mime_type ?? '';
file.width = raw?.width ?? 0;
file.height = raw?.height ?? 0;
file.imageThumbnail = raw?.mini_preview ?? '';
file.localPath = raw?.localPath ?? '';
};
return prepareBaseRecord({
action,
database,
tableName: FILE,
value,
generator,
});
};
/**
* preparePostMetadataRecord: Prepares record of entity 'POST_METADATA' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const preparePostMetadataRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawPostMetadata;
const record = value.record as PostMetadata;
const isCreateAction = action === OperationType.CREATE;
const generator = (postMeta: PostMetadata) => {
postMeta._raw.id = isCreateAction ? postMeta.id : record.id;
postMeta.data = raw.data;
postMeta.postId = raw.postId;
postMeta.type = raw.type;
};
return prepareBaseRecord({
action,
database,
tableName: POST_METADATA,
value,
generator,
});
};
/**
* prepareDraftRecord: Prepares record of entity 'DRAFT' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareDraftRecord = ({action, database, value}: DataFactoryArgs) => {
const emptyFileInfo: FileInfo[] = [];
const raw = value.raw as RawDraft;
// We use the raw id as Draft is client side only and we would only be creating/deleting drafts
const generator = (draft: Draft) => {
draft._raw.id = draft.id;
draft.rootId = raw?.root_id ?? '';
draft.message = raw?.message ?? '';
draft.channelId = raw?.channel_id ?? '';
draft.files = raw?.files ?? emptyFileInfo;
};
return prepareBaseRecord({
action,
database,
tableName: DRAFT,
value,
generator,
});
};
/**
* preparePostsInChannelRecord: Prepares record of entity 'POSTS_IN_CHANNEL' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const preparePostsInChannelRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawPostsInChannel;
const record = value.record as PostsInChannel;
const isCreateAction = action === OperationType.CREATE;
const generator = (postsInChannel: PostsInChannel) => {
postsInChannel._raw.id = isCreateAction ? postsInChannel.id : record.id;
postsInChannel.channelId = raw.channel_id;
postsInChannel.earliest = raw.earliest;
postsInChannel.latest = raw.latest;
};
return prepareBaseRecord({
action,
database,
tableName: POSTS_IN_CHANNEL,
value,
generator,
});
};

View File

@@ -0,0 +1,186 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
prepareMyTeamRecord,
prepareSlashCommandRecord,
prepareTeamChannelHistoryRecord,
prepareTeamMembershipRecord,
prepareTeamRecord,
prepareTeamSearchHistoryRecord,
} from '@database/operator/prepareRecords/team';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
describe('*** TEAM Prepare Records Test ***', () => {
it('=> prepareSlashCommandRecord: should return an array of type SlashCommand', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'team_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareSlashCommandRecord({
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('=> prepareMyTeamRecord: should return an array of type MyTeam', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'team_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareMyTeamRecord({
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('=> prepareTeamRecord: should return an array of type Team', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'team_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareTeamRecord({
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('=> prepareTeamChannelHistoryRecord: should return an array of type Team', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'team_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareTeamChannelHistoryRecord({
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('=> prepareTeamSearchHistoryRecord: should return an array of type TeamSearchHistory', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'team_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareTeamSearchHistoryRecord({
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('=> prepareTeamMembershipRecord: should return an array of type TeamMembership', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'team_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareTeamMembershipRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
team_id: 'a',
user_id: 'ab',
roles: '3ngdqe1e7tfcbmam4qgnxp91bw',
delete_at: 0,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('TeamMembership');
});
});

View File

@@ -0,0 +1,214 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
// See LICENSE.txt for license information.
import {prepareBaseRecord} from '@database/operator/prepareRecords/index';
import {
DataFactoryArgs,
RawMyTeam,
RawSlashCommand,
RawTeam,
RawTeamChannelHistory,
RawTeamMembership,
RawTeamSearchHistory,
} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
import MyTeam from '@typings/database/my_team';
import SlashCommand from '@typings/database/slash_command';
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';
const {
MY_TEAM,
SLASH_COMMAND,
TEAM,
TEAM_CHANNEL_HISTORY,
TEAM_MEMBERSHIP,
TEAM_SEARCH_HISTORY,
} = MM_TABLES.SERVER;
/**
* preparePreferenceRecord: Prepares record of entity 'TEAM_MEMBERSHIP' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareTeamMembershipRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawTeamMembership;
const record = value.record as TeamMembership;
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 generator = (teamMembership: TeamMembership) => {
teamMembership._raw.id = isCreateAction ? (raw?.id ?? teamMembership.id) : record.id;
teamMembership.teamId = raw.team_id;
teamMembership.userId = raw.user_id;
};
return prepareBaseRecord({
action,
database,
tableName: TEAM_MEMBERSHIP,
value,
generator,
});
};
/**
* prepareTeamRecord: Prepares record of entity 'TEAM' from the SERVER database for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareTeamRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawTeam;
const record = value.record as Team;
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 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 prepareBaseRecord({
action,
database,
tableName: TEAM,
value,
generator,
});
};
/**
* prepareTeamChannelHistoryRecord: Prepares record of entity 'TEAM_CHANNEL_HISTORY' from the SERVER database for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareTeamChannelHistoryRecord = ({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 prepareBaseRecord({
action,
database,
tableName: TEAM_CHANNEL_HISTORY,
value,
generator,
});
};
/**
* prepareTeamSearchHistoryRecord: Prepares record of entity 'TEAM_SEARCH_HISTORY' from the SERVER database for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareTeamSearchHistoryRecord = ({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 prepareBaseRecord({
action,
database,
tableName: TEAM_SEARCH_HISTORY,
value,
generator,
});
};
/**
* prepareSlashCommandRecord: Prepares record of entity 'SLASH_COMMAND' from the SERVER database for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareSlashCommandRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawSlashCommand;
const record = value.record as SlashCommand;
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 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 prepareBaseRecord({
action,
database,
tableName: SLASH_COMMAND,
value,
generator,
});
};
/**
* prepareMyTeamRecord: Prepares record of entity 'MY_TEAM' from the SERVER database for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareMyTeamRecord = ({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 prepareBaseRecord({
action,
database,
tableName: MY_TEAM,
value,
generator,
});
};

View File

@@ -0,0 +1,155 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
prepareChannelMembershipRecord,
preparePreferenceRecord,
prepareReactionRecord,
prepareUserRecord,
} from '@database/operator/prepareRecords/user';
// See LICENSE.txt for license information.
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
describe('*** USER Prepare Records Test ***', () => {
it('=> prepareChannelMembershipRecord: 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 prepareChannelMembershipRecord({
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_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('ChannelMembership');
});
it('=> preparePreferenceRecord: should return an array of type Preference', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'user_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await preparePreferenceRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {user_id: '9ciscaqbrpd6d8s68k76xb9bte', category: 'tutorial_step', name: '9ciscaqbrpd6d8s68k76xb9bte', value: '2'},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Preference');
});
it('=> prepareReactionRecord: should return an array of type Reaction', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'user_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareReactionRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'ps81iqbddesfby8jayz7owg4yypoo',
user_id: 'q3mzxua9zjfczqakxdkowc6u6yy',
post_id: 'ps81iqbddesfby8jayz7owg4yypoo',
emoji_name: 'thumbsup',
create_at: 1596032651748,
update_at: 1608253011321,
delete_at: 0,
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Reaction');
});
it('=> prepareUserRecord: should return an array of type User', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'user_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareUserRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: '9ciscaqbrpd6d8s68k76xb9bte',
is_bot: false,
create_at: 1599457495881,
update_at: 1607683720173,
delete_at: 0,
username: 'a.l',
auth_service: 'saml',
email: 'a.l@mattermost.com',
email_verified: true,
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,
},
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('User');
});
});

View File

@@ -0,0 +1,149 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/prepareRecords/index';
import ChannelMembership from '@typings/database/channel_membership';
import {DataFactoryArgs, RawChannelMembership, RawPreference, RawReaction, RawUser} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
import Preference from '@typings/database/preference';
import Reaction from '@typings/database/reaction';
import User from '@typings/database/user';
const {
CHANNEL_MEMBERSHIP,
PREFERENCE,
REACTION,
USER,
} = MM_TABLES.SERVER;
/**
* prepareReactionRecord: Prepares record of entity 'REACTION' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareReactionRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawReaction;
const record = value.record as Reaction;
const isCreateAction = action === OperationType.CREATE;
// id of reaction comes from server response
const generator = (reaction: Reaction) => {
reaction._raw.id = isCreateAction ? (raw?.id ?? reaction.id) : record.id;
reaction.userId = raw.user_id;
reaction.postId = raw.post_id;
reaction.emojiName = raw.emoji_name;
reaction.createAt = raw.create_at;
};
return prepareBaseRecord({
action,
database,
tableName: REACTION,
value,
generator,
});
};
/**
* prepareUserRecord: Prepares record of entity 'USER' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareUserRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawUser;
const record = value.record as User;
const isCreateAction = action === OperationType.CREATE;
// id of user comes from server response
const generator = (user: User) => {
user._raw.id = isCreateAction ? (raw?.id ?? user.id) : record.id;
user.authService = raw.auth_service;
user.deleteAt = raw.delete_at;
user.updateAt = raw.update_at;
user.email = raw.email;
user.firstName = raw.first_name;
user.isGuest = raw.roles.includes('system_guest');
user.lastName = raw.last_name;
user.lastPictureUpdate = raw.last_picture_update;
user.locale = raw.locale;
user.nickname = raw.nickname;
user.position = raw?.position ?? '';
user.roles = raw.roles;
user.username = raw.username;
user.notifyProps = raw.notify_props;
user.props = raw.props;
user.timezone = raw.timezone;
user.isBot = raw.is_bot;
};
return prepareBaseRecord({
action,
database,
tableName: USER,
value,
generator,
});
};
/**
* preparePreferenceRecord: Prepares record of entity 'PREFERENCE' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const preparePreferenceRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawPreference;
const record = value.record as Preference;
const isCreateAction = action === OperationType.CREATE;
// id of preference comes from server response
const generator = (preference: Preference) => {
preference._raw.id = isCreateAction ? (raw?.id ?? preference.id) : record.id;
preference.category = raw.category;
preference.name = raw.name;
preference.userId = raw.user_id;
preference.value = raw.value;
};
return prepareBaseRecord({
action,
database,
tableName: PREFERENCE,
value,
generator,
});
};
/**
* prepareChannelMembershipRecord: Prepares record of entity 'CHANNEL_MEMBERSHIP' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareChannelMembershipRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawChannelMembership;
const record = value.record as ChannelMembership;
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 generator = (channelMember: ChannelMembership) => {
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,
generator,
});
};

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {DatabaseType} from '@typings/database/enums';
// NOTE: uncomment the below line if you are manually testing the database
jest.mock('@database/manager');
export const createTestConnection = async ({databaseName = 'db_name', setActive = false}) => {
const serverUrl = 'https://appv2.mattermost.com';
const database = await DatabaseManager.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
dbName: databaseName,
dbType: DatabaseType.SERVER,
serverUrl,
},
});
if (setActive) {
await DatabaseManager.setActiveServerDatabase({
displayName: databaseName,
serverUrl,
});
}
return database;
};

View File

@@ -0,0 +1,98 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import Channel from '@typings/database/channel';
import {
IdenticalRecordArgs,
RangeOfValueArgs,
RawChannel,
RawPost,
RawSlashCommand,
RawTeam,
RawUser,
RawValue,
RecordPair,
RetrieveRecordsArgs,
} from '@typings/database/database';
import Post from '@typings/database/post';
import SlashCommand from '@typings/database/slash_command';
import Team from '@typings/database/team';
import User from '@typings/database/user';
const {CHANNEL, POST, SLASH_COMMAND, TEAM, USER} = MM_TABLES.SERVER;
/**
* hasSimilarUpdateAt: Database Operations on some entities are expensive. As such, we would like to operate if and only if we are
* 100% sure that the records are actually different from what we already have in the database.
* @param {IdenticalRecordArgs} identicalRecord
* @param {string} identicalRecord.tableName
* @param {RecordValue} identicalRecord.newValue
* @param {Model} identicalRecord.existingRecord
* @returns {boolean}
*/
export const hasSimilarUpdateAt = ({tableName, newValue, existingRecord}: IdenticalRecordArgs) => {
const guardTables = [CHANNEL, POST, SLASH_COMMAND, TEAM, USER];
if (guardTables.includes(tableName)) {
type Raw = RawPost | RawUser | RawTeam | RawSlashCommand | RawChannel;
type ExistingRecord = Post | User | Team | SlashCommand | Channel;
return (newValue as Raw).update_at === (existingRecord as ExistingRecord).updateAt;
}
return false;
};
/**
* This method extracts one particular field 'fieldName' from the raw values and returns them as a string array
* @param {RangeOfValueArgs} range
* @param {string} range.fieldName
* @param {RawValue[]} range.raws
* @returns {string[]}
*/
export const getRangeOfValues = ({fieldName, raws}: RangeOfValueArgs) => {
return raws.reduce((oneOfs, current: RawValue) => {
const key = fieldName as keyof typeof current;
const value: string = current[key] as string;
if (value) {
oneOfs.push(value);
}
return oneOfs;
}, [] as string[]);
};
/**
* getRawRecordPairs: Utility method that maps over the raws array to create an array of RecordPair
* @param {any[]} raws
* @returns {{record: undefined, raw: any}[]}
*/
export const getRawRecordPairs = (raws: any[]): RecordPair[] => {
return raws.map((raw) => {
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) => {
const curItemKey = item[key as keyof typeof item];
return [curItemKey, item];
})).values()];
};
/**
* retrieveRecords: Retrieves records from the database
* @param {RetrieveRecordsArgs} records
* @param {Database} records.database
* @param {string} records.tableName
* @param {any} records.condition
* @returns {Promise<Model[]>}
*/
export const retrieveRecords = ({database, tableName, condition}: RetrieveRecordsArgs) => {
return database.collections.get(tableName).query(condition).fetch();
};

View File

@@ -0,0 +1,60 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ChainPostsArgs, RawPost, RecordPair, SanitizePostsArgs} from '@typings/database/database';
/**
* sanitizePosts: Creates arrays of ordered and unordered posts. Unordered posts are those posts that are not
* present in the orders array
* @param {SanitizePostsArgs} sanitizePosts
* @param {RawPost[]} sanitizePosts.posts
* @param {string[]} sanitizePosts.orders
*/
export const sanitizePosts = ({posts, orders}: SanitizePostsArgs) => {
const orderedPosts: RawPost[] = [];
const unOrderedPosts: RawPost[] = [];
posts.forEach((post) => {
if (post?.id && orders.includes(post.id)) {
orderedPosts.push(post);
} else {
unOrderedPosts.push(post);
}
});
return {
postsOrdered: orderedPosts,
postsUnordered: unOrderedPosts,
};
};
/**
* createPostsChain: Basically creates the 'chain of posts' using the 'orders' array; each post is linked to the other
* by the previous_post_id field.
* @param {ChainPostsArgs} chainPosts
* @param {string[]} chainPosts.orders
* @param {RawPost[]} chainPosts.rawPosts
* @param {string} chainPosts.previousPostId
* @returns {RawPost[]}
*/
export const createPostsChain = ({orders, rawPosts, previousPostId = ''}: ChainPostsArgs) => {
const posts: RecordPair[] = [];
rawPosts.forEach((post) => {
const postId = post.id;
const orderIndex = orders.findIndex((order) => {
return order === postId;
});
if (orderIndex === -1) {
// This case will not occur as we are using 'ordered' posts for this step. However, if this happens, that
// implies that we might be dealing with an unordered post and in which case we do not action on it.
} else if (orderIndex === 0) {
posts.push({record: undefined, raw: {...post, prev_post_id: previousPostId}});
} else {
posts.push({record: undefined, raw: {...post, prev_post_id: orders[orderIndex - 1]}});
}
});
return posts;
};

View File

@@ -0,0 +1,67 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Q} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
import {RecordPair, SanitizeReactionsArgs} from '@typings/database/database';
import Reaction from '@typings/database/reaction';
const {REACTION} = MM_TABLES.SERVER;
/**
* sanitizeReactions: Treats reactions happening on a Post. For example, a user can add/remove an emoji. Hence, this function
* tell us which reactions to create/delete in the Reaction table and which custom-emoji to create in our database.
* For more information, please have a look at https://community.mattermost.com/core/pl/rq9e8jnonpyrmnyxpuzyc4d6ko
* @param {SanitizeReactionsArgs} sanitizeReactions
* @param {Database} sanitizeReactions.database
* @param {string} sanitizeReactions.post_id
* @param {RawReaction[]} sanitizeReactions.rawReactions
* @returns {Promise<{createReactions: RawReaction[], createEmojis: {name: string}[], deleteReactions: Reaction[]}>}
*/
export const sanitizeReactions = async ({database, post_id, rawReactions}: SanitizeReactionsArgs) => {
const reactions = (await database.collections.
get(REACTION).
query(Q.where('post_id', post_id)).
fetch()) as Reaction[];
// similarObjects: Contains objects that are in both the RawReaction array and in the Reaction entity
const similarObjects: Reaction[] = [];
const createReactions: RecordPair[] = [];
const emojiSet = new Set();
for (let i = 0; i < rawReactions.length; i++) {
const rawReaction = rawReactions[i];
// Do we have a similar value of rawReaction in the REACTION table?
const idxPresent = reactions.findIndex((value) => {
return (
value.userId === rawReaction.user_id &&
value.emojiName === rawReaction.emoji_name
);
});
if (idxPresent === -1) {
// So, we don't have a similar Reaction object. That one is new...so we'll create it
createReactions.push({record: undefined, raw: rawReaction});
// If that reaction is new, that implies that the emoji might also be new
emojiSet.add(rawReaction.emoji_name);
} else {
// we have a similar object in both reactions and rawReactions; we'll pop it out from both arrays
similarObjects.push(reactions[idxPresent]);
}
}
// finding out elements to delete using array subtract
const deleteReactions = reactions.
filter((reaction) => !similarObjects.includes(reaction)).
map((outCast) => outCast.prepareDestroyPermanently());
const createEmojis = Array.from(emojiSet).map((emoji) => {
return {name: emoji};
});
return {createReactions, createEmojis, deleteReactions};
};

View File

@@ -1,15 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DataOperator from '@database/admin/data_operator';
import DatabaseManager from '@database/admin/database_manager';
import {DataOperator} from '@database/operator';
import {createPostsChain, sanitizePosts} from '@database/operator/utils/post';
import {sanitizeReactions} from '@database/operator/utils/reaction';
import DatabaseManager from '@database/manager';
import {DatabaseType} from '@typings/database/enums';
import {RawPost} from '@typings/database/database';
import Reaction from '@typings/database/reaction';
import {createPostsChain, sanitizePosts, sanitizeReactions} from './index';
import {mockedPosts, mockedReactions} from './mock';
jest.mock('@database/admin/database_manager');
jest.mock('@database/manager');
describe('DataOperator: Utils tests', () => {
it('=> sanitizePosts: should filter between ordered and unordered posts', () => {
@@ -95,8 +97,8 @@ describe('DataOperator: Utils tests', () => {
delete_at: 0,
},
],
prepareRowsOnly: true,
});
prepareRecordsOnly: true,
}) as Reaction[];
// Jest in not using the same database instance amongst the Singletons; hence, we are creating the reaction record here
// eslint-disable-next-line max-nested-callbacks

View File

@@ -3,9 +3,9 @@
import {Database} from '@nozbe/watermelondb';
import DataOperator from '@database/admin/data_operator/handlers';
import DatabaseManager from '@database/admin/database_manager';
import DatabaseConnectionException from '@database/admin/exceptions/database_connection_exception';
import {Operator} from '@database/operator/index';
import DatabaseManager from '@database/manager';
import DatabaseConnectionException from '@database/exceptions/database_connection_exception';
export const createDataOperator = async (serverUrl: string) => {
// Retrieves the connection matching serverUrl
@@ -14,9 +14,9 @@ export const createDataOperator = async (serverUrl: string) => {
]);
if (connections?.length) {
// finds the connection that corresponds to the serverUrl value
const index = connections.findIndex((connection) => {
return connection.url === serverUrl;
// finds the connection that corresponds to the serverUrl value
const index = connections.findIndex((databaseInstance) => {
return databaseInstance.url === serverUrl;
});
if (!connections?.[index]?.dbInstance) {
@@ -27,7 +27,10 @@ export const createDataOperator = async (serverUrl: string) => {
const connection = connections[index].dbInstance as Database;
return new DataOperator(connection);
const operator = new Operator();
operator.setActiveDatabase(connection);
return operator;
}
throw new DatabaseConnectionException(

View File

@@ -1,12 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/admin/database_manager';
import {createDataOperator} from '@database/admin/data_operator/wrapper';
import DatabaseConnectionException from '@database/admin/exceptions/database_connection_exception';
import DatabaseManager from '@database/manager';
import {createDataOperator} from '@database/operator/wrapper/index';
import DatabaseConnectionException from '@database/exceptions/database_connection_exception';
import {DatabaseType} from '@typings/database/enums';
jest.mock('@database/admin/database_manager');
jest.mock('@database/manager');
/* eslint-disable @typescript-eslint/no-explicit-any */
@@ -253,7 +253,7 @@ describe('*** DataOperator Wrapper ***', () => {
delete_at: 0,
},
],
prepareRowsOnly: true,
prepareRecordsOnly: true,
});
expect(spyOnHandleFiles).toHaveBeenCalledTimes(1);
@@ -277,7 +277,7 @@ describe('*** DataOperator Wrapper ***', () => {
'/9j/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABAAEAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AN/T/iZp+pX15FpUmnwLbXtpJpyy2sQLw8CcBXA+bksCDnHGOaf4W+P3xIshbQ6loB8RrbK11f3FpbBFW3ZwiFGHB2kr25BIOeCPPbX4S3407T7rTdDfxFNIpDyRaw9lsB4OECHGR15yO4GK6fRPhR4sGmSnxAs8NgchNOjvDPsjz8qSHA37cDk5JPPFdlOpTdPlcVt/Ku1lrvr17b67EPnjrH8/626H/9k=',
},
],
prepareRowsOnly: true,
prepareRecordsOnly: true,
});
expect(spyOnHandlePostMetadata).toHaveBeenCalledTimes(1);
@@ -327,12 +327,13 @@ describe('*** DataOperator Wrapper ***', () => {
postId: '8swgtrrdiff89jnsiwiip3y1eoe',
},
],
prepareRowsOnly: true,
prepareRecordsOnly: true,
});
expect(spyOnHandleCustomEmojis).toHaveBeenCalledTimes(1);
expect(spyOnHandleCustomEmojis).toHaveBeenCalledWith({
tableName: 'CustomEmoji',
prepareRecordsOnly: false,
values: [
{
id: 'dgwyadacdbbwjc8t357h6hwsrh',

View File

@@ -11,8 +11,6 @@ export default tableSchema({
name: GROUPS_IN_TEAM,
columns: [
{name: 'group_id', type: 'string', isIndexed: true},
{name: 'member_count', type: 'number'},
{name: 'team_id', type: 'string', isIndexed: true},
{name: 'timezone_count', type: 'number'},
],
});

Some files were not shown because too many files have changed in this diff Show More