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:
parent
c25b5ab9aa
commit
78b76352c8
124 changed files with 5969 additions and 5364 deletions
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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
Loading…
Reference in a new issue