forked from Ivasoft/mattermost-mobile
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:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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()];
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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) => {
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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) => {
|
||||
302
app/database/operator/handlers/base_handler.test.ts
Normal file
302
app/database/operator/handlers/base_handler.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
456
app/database/operator/handlers/base_handler.ts
Normal file
456
app/database/operator/handlers/base_handler.ts
Normal 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;
|
||||
220
app/database/operator/handlers/channel.test.ts
Normal file
220
app/database/operator/handlers/channel.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
168
app/database/operator/handlers/channel.ts
Normal file
168
app/database/operator/handlers/channel.ts
Normal 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;
|
||||
205
app/database/operator/handlers/group.test.ts
Normal file
205
app/database/operator/handlers/group.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
162
app/database/operator/handlers/group.ts
Normal file
162
app/database/operator/handlers/group.ts
Normal 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;
|
||||
342
app/database/operator/handlers/post.test.ts
Normal file
342
app/database/operator/handlers/post.test.ts
Normal 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));
|
||||
});
|
||||
});
|
||||
491
app/database/operator/handlers/post.ts
Normal file
491
app/database/operator/handlers/post.ts
Normal 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;
|
||||
298
app/database/operator/handlers/team.test.ts
Normal file
298
app/database/operator/handlers/team.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
230
app/database/operator/handlers/team.ts
Normal file
230
app/database/operator/handlers/team.ts
Normal 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;
|
||||
332
app/database/operator/handlers/user.test.ts
Normal file
332
app/database/operator/handlers/user.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
210
app/database/operator/handlers/user.ts
Normal file
210
app/database/operator/handlers/user.ts
Normal 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;
|
||||
24
app/database/operator/index.ts
Normal file
24
app/database/operator/index.ts
Normal 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};
|
||||
133
app/database/operator/prepareRecords/channel.test.ts
Normal file
133
app/database/operator/prepareRecords/channel.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
148
app/database/operator/prepareRecords/channel.ts
Normal file
148
app/database/operator/prepareRecords/channel.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
|
||||
177
app/database/operator/prepareRecords/general.test.ts
Normal file
177
app/database/operator/prepareRecords/general.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
227
app/database/operator/prepareRecords/general.ts
Normal file
227
app/database/operator/prepareRecords/general.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
124
app/database/operator/prepareRecords/group.test.ts
Normal file
124
app/database/operator/prepareRecords/group.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
136
app/database/operator/prepareRecords/group.ts
Normal file
136
app/database/operator/prepareRecords/group.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
32
app/database/operator/prepareRecords/index.ts
Normal file
32
app/database/operator/prepareRecords/index.ts
Normal 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);
|
||||
};
|
||||
187
app/database/operator/prepareRecords/post.test.ts
Normal file
187
app/database/operator/prepareRecords/post.test.ts
Normal 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',
|
||||
);
|
||||
});
|
||||
});
|
||||
218
app/database/operator/prepareRecords/post.ts
Normal file
218
app/database/operator/prepareRecords/post.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
186
app/database/operator/prepareRecords/team.test.ts
Normal file
186
app/database/operator/prepareRecords/team.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
214
app/database/operator/prepareRecords/team.ts
Normal file
214
app/database/operator/prepareRecords/team.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
155
app/database/operator/prepareRecords/user.test.ts
Normal file
155
app/database/operator/prepareRecords/user.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
149
app/database/operator/prepareRecords/user.ts
Normal file
149
app/database/operator/prepareRecords/user.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
30
app/database/operator/utils/create_test_connection.ts
Normal file
30
app/database/operator/utils/create_test_connection.ts
Normal 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;
|
||||
};
|
||||
98
app/database/operator/utils/general.ts
Normal file
98
app/database/operator/utils/general.ts
Normal 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();
|
||||
};
|
||||
60
app/database/operator/utils/post.ts
Normal file
60
app/database/operator/utils/post.ts
Normal 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;
|
||||
};
|
||||
67
app/database/operator/utils/reaction.ts
Normal file
67
app/database/operator/utils/reaction.ts
Normal 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};
|
||||
};
|
||||
@@ -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
|
||||
@@ -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(
|
||||
@@ -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',
|
||||
@@ -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
Reference in New Issue
Block a user