[Gekidou] Refactor storage layer (#5471)

* Refactored storage layer - in progress

* Refactored DatabaseManager & Operators

* Renamed isRecordAppEqualToRaw to isRecordInfoEqualToRaw

* Review feedback

* Update app/database/models/app/info.ts

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>

* Update app/database/models/server/my_team.ts

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>

Co-authored-by: Avinash Lingaloo <>
Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
This commit is contained in:
Elias Nahum
2021-06-21 17:06:18 -04:00
committed by GitHub
parent 6f6d88f4d7
commit 17e832e689
156 changed files with 4125 additions and 5403 deletions

View File

@@ -0,0 +1,154 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Channel from '@typings/database/models/servers/channel';
import ChannelInfo from '@typings/database/models/servers/channel_info';
import ChannelMembership from '@typings/database/models/servers/channel_membership';
import CustomEmoji from '@typings/database/models/servers/custom_emoji';
import {
RawChannel,
RawChannelInfo,
RawChannelMembership,
RawCustomEmoji,
RawDraft,
RawGroup,
RawGroupMembership,
RawGroupsInChannel,
RawGroupsInTeam,
RawMyChannel,
RawMyChannelSettings,
RawMyTeam,
RawPost,
RawPreference,
RawRole,
RawSlashCommand,
RawSystem,
RawTeam,
RawTeamChannelHistory,
RawTeamMembership,
RawTeamSearchHistory,
RawTermsOfService,
RawUser,
} from '@typings/database/database';
import Draft from '@typings/database/models/servers/draft';
import Group from '@typings/database/models/servers/group';
import GroupMembership from '@typings/database/models/servers/group_membership';
import GroupsInChannel from '@typings/database/models/servers/groups_in_channel';
import GroupsInTeam from '@typings/database/models/servers/groups_in_team';
import MyChannel from '@typings/database/models/servers/my_channel';
import MyChannelSettings from '@typings/database/models/servers/my_channel_settings';
import MyTeam from '@typings/database/models/servers/my_team';
import Post from '@typings/database/models/servers/post';
import Preference from '@typings/database/models/servers/preference';
import Role from '@typings/database/models/servers/role';
import SlashCommand from '@typings/database/models/servers/slash_command';
import System from '@typings/database/models/servers/system';
import Team from '@typings/database/models/servers/team';
import TeamChannelHistory from '@typings/database/models/servers/team_channel_history';
import TeamMembership from '@typings/database/models/servers/team_membership';
import TeamSearchHistory from '@typings/database/models/servers/team_search_history';
import TermsOfService from '@typings/database/models/servers/terms_of_service';
import User from '@typings/database/models/servers/user';
/**
* This file contains all the comparators that are used by the handlers to find out which records to truly update and
* which one to create. A 'record' is a model in our database and a 'raw' is the object that is passed to the handler
* (e.g. API response). Each comparator will return a boolean condition after comparing specific fields from the
* 'record' and the 'raw'
*/
export const isRecordRoleEqualToRaw = (record: Role, raw: RawRole) => {
return raw.id === record.id;
};
export const isRecordSystemEqualToRaw = (record: System, raw: RawSystem) => {
return raw.name === record.name;
};
export const isRecordTermsOfServiceEqualToRaw = (record: TermsOfService, raw: RawTermsOfService) => {
return raw.id === record.id;
};
export const isRecordDraftEqualToRaw = (record: Draft, raw: RawDraft) => {
return raw.channel_id === record.channelId;
};
export const isRecordPostEqualToRaw = (record: Post, raw: RawPost) => {
return raw.id === record.id;
};
export const isRecordUserEqualToRaw = (record: User, raw: RawUser) => {
return raw.id === record.id;
};
export const isRecordPreferenceEqualToRaw = (record: Preference, raw: RawPreference) => {
return (
raw.category === record.category &&
raw.name === record.name &&
raw.user_id === record.userId
);
};
export const isRecordTeamMembershipEqualToRaw = (record: TeamMembership, raw: RawTeamMembership) => {
return raw.team_id === record.teamId && raw.user_id === record.userId;
};
export const isRecordCustomEmojiEqualToRaw = (record: CustomEmoji, raw: RawCustomEmoji) => {
return raw.id === record.id;
};
export const isRecordGroupMembershipEqualToRaw = (record: GroupMembership, raw: RawGroupMembership) => {
return raw.user_id === record.userId && raw.group_id === record.groupId;
};
export const isRecordChannelMembershipEqualToRaw = (record: ChannelMembership, raw: RawChannelMembership) => {
return raw.user_id === record.userId && raw.channel_id === record.channelId;
};
export const isRecordGroupEqualToRaw = (record: Group, raw: RawGroup) => {
return raw.id === record.id;
};
export const isRecordGroupsInTeamEqualToRaw = (record: GroupsInTeam, raw: RawGroupsInTeam) => {
return raw.team_id === record.teamId && raw.group_id === record.groupId;
};
export const isRecordGroupsInChannelEqualToRaw = (record: GroupsInChannel, raw: RawGroupsInChannel) => {
return raw.channel_id === record.channelId && raw.group_id === record.groupId;
};
export const isRecordTeamEqualToRaw = (record: Team, raw: RawTeam) => {
return raw.id === record.id;
};
export const isRecordTeamChannelHistoryEqualToRaw = (record: TeamChannelHistory, raw: RawTeamChannelHistory) => {
return raw.team_id === record.teamId;
};
export const isRecordTeamSearchHistoryEqualToRaw = (record: TeamSearchHistory, raw: RawTeamSearchHistory) => {
return raw.team_id === record.teamId && raw.term === record.term;
};
export const isRecordSlashCommandEqualToRaw = (record: SlashCommand, raw: RawSlashCommand) => {
return raw.id === record.id;
};
export const isRecordMyTeamEqualToRaw = (record: MyTeam, raw: RawMyTeam) => {
return raw.team_id === record.teamId;
};
export const isRecordChannelEqualToRaw = (record: Channel, raw: RawChannel) => {
return raw.id === record.id;
};
export const isRecordMyChannelSettingsEqualToRaw = (record: MyChannelSettings, raw: RawMyChannelSettings) => {
return raw.channel_id === record.channelId;
};
export const isRecordChannelInfoEqualToRaw = (record: ChannelInfo, raw: RawChannelInfo) => {
return raw.channel_id === record.channelId;
};
export const isRecordMyChannelEqualToRaw = (record: MyChannel, raw: RawMyChannel) => {
return raw.channel_id === record.channelId;
};

View File

@@ -0,0 +1,169 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {
isRecordChannelEqualToRaw,
isRecordChannelInfoEqualToRaw,
isRecordMyChannelEqualToRaw,
isRecordMyChannelSettingsEqualToRaw,
} from '@database/operator/server_data_operator/comparators';
import {
transformChannelInfoRecord,
transformChannelRecord,
transformMyChannelRecord,
transformMyChannelSettingsRecord,
} from '@database/operator/server_data_operator/transformers/channel';
import ServerDataOperator from '..';
import type {RawChannel} from '@typings/database/database';
describe('*** Operator: Channel Handlers tests ***', () => {
let operator: ServerDataOperator;
beforeAll(async () => {
await DatabaseManager.init(['baseHandler.test.com']);
operator = DatabaseManager.serverDatabases['baseHandler.test.com'].operator;
});
it('=> HandleChannel: should write to the CHANNEL table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const channels: RawChannel[] = [
{
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: '',
group_constrained: null,
shared: false,
props: null,
scheme_id: null,
},
];
await operator.handleChannel({
channels,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'id',
createOrUpdateRawValues: channels,
tableName: 'Channel',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordChannelEqualToRaw,
transformer: transformChannelRecord,
});
});
it('=> HandleMyChannelSettings: should write to the MY_CHANNEL_SETTINGS table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const settings = [
{
channel_id: 'c',
notify_props: {
desktop: 'all',
desktop_sound: true,
email: true,
first_name: true,
mention_keys: '',
push: 'mention',
channel: true,
},
},
];
await operator.handleMyChannelSettings({
settings,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'channel_id',
createOrUpdateRawValues: settings,
tableName: 'MyChannelSettings',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordMyChannelSettingsEqualToRaw,
transformer: transformMyChannelSettingsRecord,
});
});
it('=> HandleChannelInfo: should write to the CHANNEL_INFO table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator as any, 'handleRecords');
const channelInfos = [
{
channel_id: 'c',
guest_count: 10,
header: 'channel info header',
member_count: 10,
pinned_post_count: 3,
purpose: 'sample channel ',
},
];
await operator.handleChannelInfo({
channelInfos,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'channel_id',
createOrUpdateRawValues: channelInfos,
tableName: 'ChannelInfo',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordChannelInfoEqualToRaw,
transformer: transformChannelInfoRecord,
});
});
it('=> HandleMyChannel: should write to the MY_CHANNEL table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const myChannels = [
{
channel_id: 'c',
last_post_at: 1617311494451,
last_viewed_at: 1617311494451,
mentions_count: 3,
message_count: 10,
roles: 'guest',
},
];
await operator.handleMyChannel({
myChannels,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'channel_id',
createOrUpdateRawValues: myChannels,
tableName: 'MyChannel',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordMyChannelEqualToRaw,
transformer: transformMyChannelRecord,
});
});
});

View File

@@ -0,0 +1,176 @@
// 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/server_data_operator/comparators';
import {
transformChannelInfoRecord,
transformChannelRecord,
transformMyChannelRecord,
transformMyChannelSettingsRecord,
} from '@database/operator/server_data_operator/transformers/channel';
import {getUniqueRawsBy} from '@database/operator/utils/general';
import Channel from '@typings/database/models/servers/channel';
import ChannelInfo from '@typings/database/models/servers/channel_info';
import {
HandleChannelArgs,
HandleChannelInfoArgs,
HandleMyChannelArgs,
HandleMyChannelSettingsArgs,
} from '@typings/database/database';
import MyChannel from '@typings/database/models/servers/my_channel';
import MyChannelSettings from '@typings/database/models/servers/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 table from the 'Server' schema
* @param {HandleChannelArgs} channelsArgs
* @param {RawChannel[]} channelsArgs.channels
* @param {boolean} channelsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {Channel[]}
*/
handleChannel = async ({channels, prepareRecordsOnly = true}: HandleChannelArgs) => {
let records: Channel[] = [];
if (!channels.length) {
throw new DataOperatorException(
'An empty "channels" array has been passed to the handleChannel method',
);
}
const createOrUpdateRawValues = getUniqueRawsBy({raws: channels, key: 'id'});
records = await this.handleRecords({
fieldName: 'id',
findMatchingRecordBy: isRecordChannelEqualToRaw,
transformer: transformChannelRecord,
prepareRecordsOnly,
createOrUpdateRawValues,
tableName: CHANNEL,
});
return records;
};
/**
* handleMyChannelSettings: Handler responsible for the Create/Update operations occurring on the MY_CHANNEL_SETTINGS table from the 'Server' schema
* @param {HandleMyChannelSettingsArgs} settingsArgs
* @param {RawMyChannelSettings[]} settingsArgs.settings
* @param {boolean} settingsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {MyChannelSettings[]}
*/
handleMyChannelSettings = async ({settings, prepareRecordsOnly = true}: HandleMyChannelSettingsArgs) => {
let records: MyChannelSettings[] = [];
if (!settings.length) {
throw new DataOperatorException(
'An empty "settings" array has been passed to the handleMyChannelSettings method',
);
}
const createOrUpdateRawValues = getUniqueRawsBy({raws: settings, key: 'channel_id'});
records = await this.handleRecords({
fieldName: 'channel_id',
findMatchingRecordBy: isRecordMyChannelSettingsEqualToRaw,
transformer: transformMyChannelSettingsRecord,
prepareRecordsOnly,
createOrUpdateRawValues,
tableName: MY_CHANNEL_SETTINGS,
});
return records;
};
/**
* handleChannelInfo: Handler responsible for the Create/Update operations occurring on the CHANNEL_INFO table from the 'Server' schema
* @param {HandleChannelInfoArgs} channelInfosArgs
* @param {RawChannelInfo[]} channelInfosArgs.channelInfos
* @param {boolean} channelInfosArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {ChannelInfo[]}
*/
handleChannelInfo = async ({channelInfos, prepareRecordsOnly = true}: HandleChannelInfoArgs) => {
let records: ChannelInfo[] = [];
if (!channelInfos.length) {
throw new DataOperatorException(
'An empty "channelInfos" array has been passed to the handleMyChannelSettings method',
);
}
const createOrUpdateRawValues = getUniqueRawsBy({
raws: channelInfos,
key: 'channel_id',
});
records = await this.handleRecords({
fieldName: 'channel_id',
findMatchingRecordBy: isRecordChannelInfoEqualToRaw,
transformer: transformChannelInfoRecord,
prepareRecordsOnly,
createOrUpdateRawValues,
tableName: CHANNEL_INFO,
});
return records;
};
/**
* handleMyChannel: Handler responsible for the Create/Update operations occurring on the MY_CHANNEL table from the 'Server' schema
* @param {HandleMyChannelArgs} myChannelsArgs
* @param {RawMyChannel[]} myChannelsArgs.myChannels
* @param {boolean} myChannelsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {MyChannel[]}
*/
handleMyChannel = async ({myChannels, prepareRecordsOnly = true}: HandleMyChannelArgs) => {
let records: MyChannel[] = [];
if (!myChannels.length) {
throw new DataOperatorException(
'An empty "myChannels" array has been passed to the handleMyChannel method',
);
}
const createOrUpdateRawValues = getUniqueRawsBy({
raws: myChannels,
key: 'channel_id',
});
records = await this.handleRecords({
fieldName: 'channel_id',
findMatchingRecordBy: isRecordMyChannelEqualToRaw,
transformer: transformMyChannelRecord,
prepareRecordsOnly,
createOrUpdateRawValues,
tableName: MY_CHANNEL,
});
return records;
};
};
export default ChannelHandler;

View File

@@ -0,0 +1,159 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {
isRecordGroupEqualToRaw,
isRecordGroupMembershipEqualToRaw,
isRecordGroupsInChannelEqualToRaw,
isRecordGroupsInTeamEqualToRaw,
} from '@database/operator/server_data_operator/comparators';
import {
transformGroupMembershipRecord,
transformGroupRecord,
transformGroupsInChannelRecord,
transformGroupsInTeamRecord,
} from '@database/operator/server_data_operator/transformers/group';
import ServerDataOperator from '..';
describe('*** Operator: Group Handlers tests ***', () => {
let operator: ServerDataOperator;
beforeAll(async () => {
await DatabaseManager.init(['baseHandler.test.com']);
operator = DatabaseManager.serverDatabases['baseHandler.test.com'].operator;
});
it('=> HandleGroup: should write to the GROUP table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const 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,
},
];
await operator.handleGroup({
groups,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'name',
createOrUpdateRawValues: groups,
tableName: 'Group',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupEqualToRaw,
transformer: transformGroupRecord,
});
});
it('=> HandleGroupsInTeam: should write to the GROUPS_IN_TEAM table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const 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,
},
];
await operator.handleGroupsInTeam({
groupsInTeams,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'group_id',
createOrUpdateRawValues: groupsInTeams,
tableName: 'GroupsInTeam',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupsInTeamEqualToRaw,
transformer: transformGroupsInTeamRecord,
});
});
it('=> HandleGroupsInChannel: should write to the GROUPS_IN_CHANNEL table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const 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,
},
];
await operator.handleGroupsInChannel({
groupsInChannels,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'group_id',
createOrUpdateRawValues: groupsInChannels,
tableName: 'GroupsInChannel',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupsInChannelEqualToRaw,
transformer: transformGroupsInChannelRecord,
});
});
it('=> HandleGroupMembership: should write to the GROUP_MEMBERSHIP table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const groupMemberships = [
{
user_id: 'u4cprpki7ri81mbx8efixcsb8jo',
group_id: 'g4cprpki7ri81mbx8efixcsb8jo',
},
];
await operator.handleGroupMembership({
groupMemberships,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
createOrUpdateRawValues: groupMemberships,
tableName: 'GroupMembership',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupMembershipEqualToRaw,
transformer: transformGroupMembershipRecord,
});
});
});

View File

@@ -0,0 +1,170 @@
// 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/server_data_operator/comparators';
import {
transformGroupMembershipRecord,
transformGroupRecord,
transformGroupsInChannelRecord,
transformGroupsInTeamRecord,
} from '@database/operator/server_data_operator/transformers/group';
import {getUniqueRawsBy} from '@database/operator/utils/general';
import {
HandleGroupArgs,
HandleGroupMembershipArgs,
HandleGroupsInChannelArgs,
HandleGroupsInTeamArgs,
} from '@typings/database/database';
import Group from '@typings/database/models/servers/group';
import GroupMembership from '@typings/database/models/servers/group_membership';
import GroupsInChannel from '@typings/database/models/servers/groups_in_channel';
import GroupsInTeam from '@typings/database/models/servers/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 table from the 'Server' schema
* @param {HandleGroupMembershipArgs} groupMembershipsArgs
* @param {RawGroupMembership[]} groupMembershipsArgs.groupMemberships
* @param {boolean} groupMembershipsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {GroupMembership[]}
*/
handleGroupMembership = async ({groupMemberships, prepareRecordsOnly = true}: HandleGroupMembershipArgs) => {
let records: GroupMembership[] = [];
if (!groupMemberships.length) {
throw new DataOperatorException(
'An empty "groupMemberships" array has been passed to the handleGroupMembership method',
);
}
const createOrUpdateRawValues = getUniqueRawsBy({raws: groupMemberships, key: 'group_id'});
records = await this.handleRecords({
fieldName: 'user_id',
findMatchingRecordBy: isRecordGroupMembershipEqualToRaw,
transformer: transformGroupMembershipRecord,
prepareRecordsOnly,
createOrUpdateRawValues,
tableName: GROUP_MEMBERSHIP,
});
return records;
};
/**
* handleGroup: Handler responsible for the Create/Update operations occurring on the GROUP table from the 'Server' schema
* @param {HandleGroupArgs} groupsArgs
* @param {RawGroup[]} groupsArgs.groups
* @param {boolean} groupsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {Group[]}
*/
handleGroup = async ({groups, prepareRecordsOnly = true}: HandleGroupArgs) => {
let records: Group[] = [];
if (!groups.length) {
throw new DataOperatorException(
'An empty "groups" array has been passed to the handleGroup method',
);
}
const createOrUpdateRawValues = getUniqueRawsBy({raws: groups, key: 'name'});
records = await this.handleRecords({
fieldName: 'name',
findMatchingRecordBy: isRecordGroupEqualToRaw,
transformer: transformGroupRecord,
prepareRecordsOnly,
createOrUpdateRawValues,
tableName: GROUP,
});
return records;
};
/**
* handleGroupsInTeam: Handler responsible for the Create/Update operations occurring on the GROUPS_IN_TEAM table from the 'Server' schema
* @param {HandleGroupsInTeamArgs} groupsInTeamsArgs
* @param {RawGroupsInTeam[]} groupsInTeamsArgs.groupsInTeams
* @param {boolean} groupsInTeamsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {GroupsInTeam[]}
*/
handleGroupsInTeam = async ({groupsInTeams, prepareRecordsOnly = true} : HandleGroupsInTeamArgs) => {
let records: GroupsInTeam[] = [];
if (!groupsInTeams.length) {
throw new DataOperatorException(
'An empty "groups" array has been passed to the handleGroupsInTeam method',
);
}
const createOrUpdateRawValues = getUniqueRawsBy({raws: groupsInTeams, key: 'group_id'});
records = await this.handleRecords({
fieldName: 'group_id',
findMatchingRecordBy: isRecordGroupsInTeamEqualToRaw,
transformer: transformGroupsInTeamRecord,
prepareRecordsOnly,
createOrUpdateRawValues,
tableName: GROUPS_IN_TEAM,
});
return records;
};
/**
* handleGroupsInChannel: Handler responsible for the Create/Update operations occurring on the GROUPS_IN_CHANNEL table from the 'Server' schema
* @param {HandleGroupsInChannelArgs} groupsInChannelsArgs
* @param {RawGroupsInChannel[]} groupsInChannelsArgs.groupsInChannels
* @param {boolean} groupsInChannelsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {GroupsInChannel[]}
*/
handleGroupsInChannel = async ({groupsInChannels, prepareRecordsOnly = true}: HandleGroupsInChannelArgs) => {
let records: GroupsInChannel[] = [];
if (!groupsInChannels.length) {
throw new DataOperatorException(
'An empty "groups" array has been passed to the handleGroupsInTeam method',
);
}
const createOrUpdateRawValues = getUniqueRawsBy({raws: groupsInChannels, key: 'channel_id'});
records = await this.handleRecords({
fieldName: 'group_id',
findMatchingRecordBy: isRecordGroupsInChannelEqualToRaw,
transformer: transformGroupsInChannelRecord,
prepareRecordsOnly,
createOrUpdateRawValues,
tableName: GROUPS_IN_CHANNEL,
});
return records;
};
};
export default GroupHandler;

View File

@@ -0,0 +1,158 @@
// 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 {
isRecordCustomEmojiEqualToRaw,
isRecordRoleEqualToRaw,
isRecordSystemEqualToRaw,
isRecordTermsOfServiceEqualToRaw,
} from '@database/operator/server_data_operator/comparators';
import {
transformCustomEmojiRecord,
transformRoleRecord,
transformSystemRecord,
transformTermsOfServiceRecord,
} from '@database/operator/server_data_operator/transformers/general';
import {RawRole, RawTermsOfService} from '@typings/database/database';
import ServerDataOperator from '..';
describe('*** DataOperator: Base Handlers tests ***', () => {
let operator: ServerDataOperator;
beforeAll(async () => {
await DatabaseManager.init(['baseHandler.test.com']);
operator = DatabaseManager.serverDatabases['baseHandler.test.com'].operator;
});
it('=> HandleRole: should write to the ROLE table', async () => {
expect.assertions(1);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const roles: RawRole[] = [
{
id: 'custom-role-id-1',
name: 'custom-role-1',
permissions: ['custom-permission-1'],
},
];
await operator.handleRole({
roles,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'id',
transformer: transformRoleRecord,
findMatchingRecordBy: isRecordRoleEqualToRaw,
createOrUpdateRawValues: roles,
tableName: 'Role',
prepareRecordsOnly: false,
});
});
it('=> HandleCustomEmojis: should write to the CUSTOM_EMOJI table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const emojis = [
{
id: 'i',
create_at: 1580913641769,
update_at: 1580913641769,
delete_at: 0,
creator_id: '4cprpki7ri81mbx8efixcsb8jo',
name: 'boomI',
},
];
await operator.handleCustomEmojis({
emojis,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'id',
createOrUpdateRawValues: emojis,
tableName: 'CustomEmoji',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordCustomEmojiEqualToRaw,
transformer: transformCustomEmojiRecord,
});
});
it('=> HandleSystem: should write to the SYSTEM table', async () => {
expect.assertions(1);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const systems = [{name: 'system-1', value: 'system-1'}];
await operator.handleSystem({
systems,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledWith({
findMatchingRecordBy: isRecordSystemEqualToRaw,
fieldName: 'name',
transformer: transformSystemRecord,
createOrUpdateRawValues: systems,
tableName: 'System',
prepareRecordsOnly: false,
});
});
it('=> HandleTermsOfService: should write to the TERMS_OF_SERVICE table', async () => {
expect.assertions(1);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const termOfService: RawTermsOfService[] = [
{
id: 'tos-1',
accepted_at: 1,
create_at: 1613667352029,
user_id: 'user1613667352029',
text: '',
},
];
await operator.handleTermOfService({
termOfService,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledWith({
findMatchingRecordBy: isRecordTermsOfServiceEqualToRaw,
fieldName: 'id',
transformer: transformTermsOfServiceRecord,
createOrUpdateRawValues: termOfService,
tableName: 'TermsOfService',
prepareRecordsOnly: false,
});
});
it('=> No table name: should not call execute if tableName is invalid', async () => {
expect.assertions(3);
const appDatabase = DatabaseManager.appDatabase?.database;
const appOperator = DatabaseManager.appDatabase?.operator;
expect(appDatabase).toBeTruthy();
expect(appOperator).toBeTruthy();
await expect(
operator?.handleRecords({
fieldName: 'invalidField',
tableName: 'INVALID_TABLE_NAME',
// @ts-expect-error: Type does not match RawValue
createOrUpdateRawValues: [{id: 'tos-1', accepted_at: 1}],
}),
).rejects.toThrow(DataOperatorException);
});
});

View File

@@ -0,0 +1,122 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import BaseDataOperator from '@database/operator/base_data_operator';
import DataOperatorException from '@database/exceptions/data_operator_exception';
import {
isRecordCustomEmojiEqualToRaw,
isRecordRoleEqualToRaw,
isRecordSystemEqualToRaw,
isRecordTermsOfServiceEqualToRaw,
} from '@database/operator/server_data_operator/comparators';
import {
transformCustomEmojiRecord,
transformRoleRecord,
transformSystemRecord,
transformTermsOfServiceRecord,
} from '@database/operator/server_data_operator/transformers/general';
import {getUniqueRawsBy} from '@database/operator/utils/general';
import {HandleCustomEmojiArgs, HandleRoleArgs, HandleSystemArgs, HandleTOSArgs, OperationArgs} from '@typings/database/database';
const {SERVER: {CUSTOM_EMOJI, ROLE, SYSTEM, TERMS_OF_SERVICE}} = MM_TABLES;
export default class ServerDataOperatorBase extends BaseDataOperator {
handleRole = async ({roles, prepareRecordsOnly = true}: HandleRoleArgs) => {
if (!roles.length) {
throw new DataOperatorException(
'An empty "values" array has been passed to the handleRole',
);
}
const records = await this.handleRecords({
fieldName: 'id',
findMatchingRecordBy: isRecordRoleEqualToRaw,
transformer: transformRoleRecord,
prepareRecordsOnly,
createOrUpdateRawValues: getUniqueRawsBy({raws: roles, key: 'id'}),
tableName: ROLE,
});
return records;
}
handleCustomEmojis = async ({emojis, prepareRecordsOnly = true}: HandleCustomEmojiArgs) => {
if (!emojis.length) {
throw new DataOperatorException(
'An empty "values" array has been passed to the handleCustomEmojis',
);
}
const records = await this.handleRecords({
fieldName: 'id',
findMatchingRecordBy: isRecordCustomEmojiEqualToRaw,
transformer: transformCustomEmojiRecord,
prepareRecordsOnly,
createOrUpdateRawValues: getUniqueRawsBy({raws: emojis, key: 'id'}),
tableName: CUSTOM_EMOJI,
});
return records;
}
handleSystem = async ({systems, prepareRecordsOnly = true}: HandleSystemArgs) => {
if (!systems.length) {
throw new DataOperatorException(
'An empty "values" array has been passed to the handleSystem',
);
}
const records = await this.handleRecords({
fieldName: 'name',
findMatchingRecordBy: isRecordSystemEqualToRaw,
transformer: transformSystemRecord,
prepareRecordsOnly,
createOrUpdateRawValues: getUniqueRawsBy({raws: systems, key: 'name'}),
tableName: SYSTEM,
});
return records;
}
handleTermOfService = async ({termOfService, prepareRecordsOnly = true}: HandleTOSArgs) => {
if (!termOfService.length) {
throw new DataOperatorException(
'An empty "values" array has been passed to the handleTermOfService',
);
}
const records = await this.handleRecords({
fieldName: 'id',
findMatchingRecordBy: isRecordTermsOfServiceEqualToRaw,
transformer: transformTermsOfServiceRecord,
prepareRecordsOnly,
createOrUpdateRawValues: getUniqueRawsBy({raws: termOfService, key: 'id'}),
tableName: TERMS_OF_SERVICE,
});
return records;
}
/**
* execute: Handles the Create/Update operations on an table.
* @param {OperationArgs} execute
* @param {string} execute.tableName
* @param {RecordValue[]} execute.createRaws
* @param {RecordValue[]} execute.updateRaws
* @param {(TransformerArgs) => Promise<Model>} execute.recordOperator
* @returns {Promise<void>}
*/
execute = async ({createRaws, transformer, tableName, updateRaws}: OperationArgs) => {
const models = await this.prepareRecords({
tableName,
createRaws,
updateRaws,
transformer,
});
if (models?.length > 0) {
await this.batchRecords(models);
}
};
}

View File

@@ -0,0 +1,342 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {isRecordDraftEqualToRaw} from '@database/operator/server_data_operator/comparators';
import {transformDraftRecord} from '@database/operator/server_data_operator/transformers/post';
import ServerDataOperator from '..';
describe('*** Operator: Post Handlers tests ***', () => {
let operator: ServerDataOperator;
beforeAll(async () => {
await DatabaseManager.init(['baseHandler.test.com']);
operator = DatabaseManager.serverDatabases['baseHandler.test.com'].operator;
});
it('=> HandleDraft: should write to the the Draft table', async () => {
expect.assertions(1);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const drafts = [
{
channel_id: '4r9jmr7eqt8dxq3f9woypzurrychannelid',
files: [
{
id: '322dxx',
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 operator.handleDraft({drafts, prepareRecordsOnly: false});
expect(spyOnHandleRecords).toHaveBeenCalledWith({
findMatchingRecordBy: isRecordDraftEqualToRaw,
fieldName: 'channel_id',
transformer: transformDraftRecord,
createOrUpdateRawValues: drafts,
tableName: 'Draft',
prepareRecordsOnly: false,
});
});
it('=> HandlePosts: should write to the Post and its sub-child tables', 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(operator, 'handleFiles');
const spyOnHandlePostMetadata = jest.spyOn(operator, 'handlePostMetadata');
const spyOnHandleReactions = jest.spyOn(operator, 'handleReactions');
const spyOnHandleCustomEmojis = jest.spyOn(operator, 'handleCustomEmojis');
const spyOnHandlePostsInThread = jest.spyOn(operator, 'handlePostsInThread');
const spyOnHandlePostsInChannel = jest.spyOn(operator, 'handlePostsInChannel');
// handlePosts will in turn call handlePostsInThread
await operator.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({
prepareRecordsOnly: false,
emojis: [
{
id: 'dgwyadacdbbwjc8t357h6hwsrh',
create_at: 1502389307432,
update_at: 1502389307432,
delete_at: 0,
creator_id: 'x6sdh1ok1tyd9f4dgq4ybw839a',
name: 'thanks',
},
],
});
expect(spyOnHandlePostsInThread).toHaveBeenCalledTimes(1);
expect(spyOnHandlePostsInThread).toHaveBeenCalledWith([
{earliest: 1596032651747, post_id: '8swgtrrdiff89jnsiwiip3y1eoe'},
]);
expect(spyOnHandlePostsInChannel).toHaveBeenCalledTimes(1);
expect(spyOnHandlePostsInChannel).toHaveBeenCalledWith(posts.slice(0, 3));
});
});

View File

@@ -0,0 +1,478 @@
// 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 DataOperatorException from '@database/exceptions/data_operator_exception';
import {isRecordDraftEqualToRaw, isRecordPostEqualToRaw} from '@database/operator/server_data_operator/comparators';
import {
transformDraftRecord,
transformFileRecord,
transformPostInThreadRecord,
transformPostMetadataRecord,
transformPostRecord,
transformPostsInChannelRecord,
} from '@database/operator/server_data_operator/transformers/post';
import {getRawRecordPairs, getUniqueRawsBy, retrieveRecords} from '@database/operator/utils/general';
import {createPostsChain, sanitizePosts} from '@database/operator/utils/post';
import {
HandleDraftArgs,
HandleFilesArgs,
HandlePostMetadataArgs,
HandlePostsArgs,
PostImage,
RawCustomEmoji,
RawEmbed,
RawFile,
RawPost,
RawPostMetadata,
RawPostsInThread,
RawReaction, RecordPair,
} from '@typings/database/database';
import Draft from '@typings/database/models/servers/draft';
import File from '@typings/database/models/servers/file';
import Post from '@typings/database/models/servers/post';
import PostMetadata from '@typings/database/models/servers/post_metadata';
import PostsInChannel from '@typings/database/models/servers/posts_in_channel';
import PostsInThread from '@typings/database/models/servers/posts_in_thread';
import Reaction from '@typings/database/models/servers/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 table from the 'Server' schema
* @param {HandleDraftArgs} draftsArgs
* @param {RawDraft[]} draftsArgs.drafts
* @param {boolean} draftsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {Draft[]}
*/
handleDraft = async ({drafts, prepareRecordsOnly = true}: HandleDraftArgs) => {
let records: Draft[] = [];
if (!drafts.length) {
throw new DataOperatorException(
'An empty "drafts" array has been passed to the handleDraft method',
);
}
const createOrUpdateRawValues = getUniqueRawsBy({raws: drafts, key: 'channel_id'});
records = await this.handleRecords({
fieldName: 'channel_id',
findMatchingRecordBy: isRecordDraftEqualToRaw,
transformer: transformDraftRecord,
prepareRecordsOnly,
createOrUpdateRawValues,
tableName: DRAFT,
});
return records;
};
/**
* handlePosts: Handler responsible for the Create/Update operations occurring on the Post table 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.processRecords({
createOrUpdateRawValues: 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,
});
// Prepares records for batch processing onto the 'Post' table for the server schema
const posts = (await this.prepareRecords({
createRaws: linkedRawPosts,
transformer: transformPostRecord,
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 tables' 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.batchRecords(batch);
}
// LAST: calls handler for CustomEmojis, PostsInThread, PostsInChannel
if (emojis.length) {
await this.handleCustomEmojis({
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.handleRecords({
findMatchingRecordBy: isRecordPostEqualToRaw,
fieldName: 'id',
trasformer: transformPostRecord,
createOrUpdateRawValues: postsUnordered,
tableName: POST,
prepareRecordsOnly: false,
});
}
};
/**
* handleFiles: Handler responsible for the Create/Update operations occurring on the File table 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 postFiles = await this.prepareRecords({
createRaws: getRawRecordPairs(files),
transformer: transformFileRecord,
tableName: FILE,
});
if (prepareRecordsOnly) {
return postFiles;
}
if (postFiles?.length) {
await this.batchRecords(postFiles);
}
return [];
};
/**
* handlePostMetadata: Handler responsible for the Create/Update operations occurring on the PostMetadata table 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 postMetas = await this.prepareRecords({
createRaws: getRawRecordPairs(metadata),
transformer: transformPostMetadataRecord,
tableName: POST_METADATA,
});
if (prepareRecordsOnly) {
return postMetas;
}
if (postMetas?.length) {
await this.batchRecords(postMetas);
}
return [];
};
/**
* handlePostsInThread: Handler responsible for the Create/Update operations occurring on the PostsInThread table 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[] = [];
// Retrieves all threads whereby their root_id can be one of the element in the postIds array
const threads = (await this.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),
transformer: transformPostInThreadRecord,
tableName: POSTS_IN_THREAD,
})) as PostsInThread[];
if (postInThreadRecords?.length) {
await this.batchRecords(postInThreadRecords);
}
}
};
/**
* handlePostsInChannel: Handler responsible for the Create/Update operations occurring on the PostsInChannel table 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;
// 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: this.database,
tableName: POSTS_IN_CHANNEL,
condition: Q.where('channel_id', channelId),
})) as PostsInChannel[];
const createPostsInChannelRecord = async () => {
await this.execute({
createRaws: [{record: undefined, raw: {channel_id: channelId, earliest, latest}}],
tableName: POSTS_IN_CHANNEL,
transformer: transformPostsInChannelRecord,
});
};
// 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: this.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 this.database.action(async () => {
await targetChunk.update((postInChannel) => {
postInChannel.earliest = earliest;
});
});
} else {
await createPostsInChannelRecord();
return [];
}
}
} else {
await createPostsInChannelRecord();
return [];
}
return [];
};
};
export default PostHandler;

View File

@@ -0,0 +1,231 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {
isRecordMyTeamEqualToRaw,
isRecordSlashCommandEqualToRaw,
isRecordTeamChannelHistoryEqualToRaw,
isRecordTeamEqualToRaw,
isRecordTeamMembershipEqualToRaw,
isRecordTeamSearchHistoryEqualToRaw,
} from '@database/operator/server_data_operator/comparators';
import {
transformMyTeamRecord,
transformSlashCommandRecord,
transformTeamChannelHistoryRecord,
transformTeamMembershipRecord,
transformTeamRecord,
transformTeamSearchHistoryRecord,
} from '@database/operator/server_data_operator/transformers/team';
import ServerDataOperator from '..';
describe('*** Operator: Team Handlers tests ***', () => {
let operator: ServerDataOperator;
beforeAll(async () => {
await DatabaseManager.init(['baseHandler.test.com']);
operator = DatabaseManager.serverDatabases['baseHandler.test.com'].operator;
});
it('=> HandleTeam: should write to the TEAM table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const 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,
},
];
await operator.handleTeam({
teams,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'id',
createOrUpdateRawValues: teams,
tableName: 'Team',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamEqualToRaw,
transformer: transformTeamRecord,
});
});
it('=> HandleTeamMemberships: should write to the TEAM_MEMBERSHIP table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const teamMemberships = [
{
team_id: 'a',
user_id: 'ab',
roles: '3ngdqe1e7tfcbmam4qgnxp91bw',
delete_at: 0,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
];
await operator.handleTeamMemberships({
teamMemberships,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
createOrUpdateRawValues: teamMemberships,
tableName: 'TeamMembership',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamMembershipEqualToRaw,
transformer: transformTeamMembershipRecord,
});
});
it('=> HandleMyTeam: should write to the MY_TEAM table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const myTeams = [
{
team_id: 'teamA',
roles: 'roleA, roleB, roleC',
is_unread: true,
mentions_count: 3,
},
];
await operator.handleMyTeam({
myTeams,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'team_id',
createOrUpdateRawValues: myTeams,
tableName: 'MyTeam',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordMyTeamEqualToRaw,
transformer: transformMyTeamRecord,
});
});
it('=> HandleTeamChannelHistory: should write to the TEAM_CHANNEL_HISTORY table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const teamChannelHistories = [
{
team_id: 'a',
channel_ids: ['ca', 'cb'],
},
];
await operator.handleTeamChannelHistory({
teamChannelHistories,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'team_id',
createOrUpdateRawValues: teamChannelHistories,
tableName: 'TeamChannelHistory',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamChannelHistoryEqualToRaw,
transformer: transformTeamChannelHistoryRecord,
});
});
it('=> HandleTeamSearchHistory: should write to the TEAM_SEARCH_HISTORY table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const teamSearchHistories = [
{
team_id: 'a',
term: 'termA',
display_term: 'termA',
created_at: 1445538153952,
},
];
await operator.handleTeamSearchHistory({
teamSearchHistories,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'team_id',
createOrUpdateRawValues: teamSearchHistories,
tableName: 'TeamSearchHistory',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamSearchHistoryEqualToRaw,
transformer: transformTeamSearchHistoryRecord,
});
});
it('=> HandleSlashCommand: should write to the SLASH_COMMAND table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const 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',
},
];
await operator.handleSlashCommand({
slashCommands,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'id',
createOrUpdateRawValues: slashCommands,
tableName: 'SlashCommand',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordSlashCommandEqualToRaw,
transformer: transformSlashCommandRecord,
});
});
});

View File

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

View File

@@ -0,0 +1,222 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import ServerDataOperator from '@database/operator/server_data_operator';
import {
isRecordChannelMembershipEqualToRaw,
isRecordPreferenceEqualToRaw,
isRecordUserEqualToRaw,
} from '@database/operator/server_data_operator/comparators';
import {
transformChannelMembershipRecord,
transformPreferenceRecord,
transformUserRecord,
} from '@database/operator/server_data_operator/transformers/user';
describe('*** Operator: User Handlers tests ***', () => {
let operator: ServerDataOperator;
beforeAll(async () => {
await DatabaseManager.init(['baseHandler.test.com']);
operator = DatabaseManager.serverDatabases['baseHandler.test.com'].operator;
});
it('=> HandleReactions: should write to both Reactions and CustomEmoji tables', async () => {
expect.assertions(2);
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
const spyOnBatchOperation = jest.spyOn(operator, 'batchRecords');
await operator.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 tables
expect(spyOnBatchOperation).toHaveBeenCalledTimes(1);
});
it('=> HandleUsers: should write to the User table', 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: '',
},
},
];
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
await operator.handleUsers({users, prepareRecordsOnly: false});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'id',
createOrUpdateRawValues: users,
tableName: 'User',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordUserEqualToRaw,
transformer: transformUserRecord,
});
});
it('=> HandlePreferences: should write to the PREFERENCE table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const 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',
},
];
await operator.handlePreferences({
preferences,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
createOrUpdateRawValues: preferences,
tableName: 'Preference',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordPreferenceEqualToRaw,
transformer: transformPreferenceRecord,
});
});
it('=> HandleChannelMembership: should write to the CHANNEL_MEMBERSHIP table', 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 spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
await operator.handleChannelMembership({
channelMemberships,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
createOrUpdateRawValues: channelMemberships,
tableName: 'ChannelMembership',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordChannelMembershipEqualToRaw,
transformer: transformChannelMembershipRecord,
});
});
});

View File

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

View File

@@ -0,0 +1,29 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import ServerDataOperatorBase from '@database/operator/server_data_operator/handlers';
import ChannelHandler, {ChannelHandlerMix} from '@database/operator/server_data_operator/handlers/channel';
import GroupHandler, {GroupHandlerMix} from '@database/operator/server_data_operator/handlers/group';
import PostHandler, {PostHandlerMix} from '@database/operator/server_data_operator/handlers/post';
import TeamHandler, {TeamHandlerMix} from '@database/operator/server_data_operator/handlers/team';
import UserHandler, {UserHandlerMix} from '@database/operator/server_data_operator/handlers/user';
import mix from '@utils/mix';
import type {Database} from '@nozbe/watermelondb';
interface ServerDataOperator extends ServerDataOperatorBase, PostHandlerMix, UserHandlerMix, GroupHandlerMix, ChannelHandlerMix, TeamHandlerMix {}
class ServerDataOperator extends mix(ServerDataOperatorBase).with(
ChannelHandler,
GroupHandler,
PostHandler,
TeamHandler,
UserHandler,
) {
// eslint-disable-next-line no-useless-constructor
constructor(database: Database) {
super(database);
}
}
export default ServerDataOperator;

View File

@@ -0,0 +1,133 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
transformChannelInfoRecord,
transformChannelRecord,
transformMyChannelRecord,
transformMyChannelSettingsRecord,
} from '@database/operator/server_data_operator/transformers/channel';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
describe('*** CHANNEL Prepare Records Test ***', () => {
it('=> transformChannelRecord: 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 transformChannelRecord({
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('=> transformMyChannelSettingsRecord: 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 transformMyChannelSettingsRecord({
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('=> transformChannelInfoRecord: 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 transformChannelInfoRecord({
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('=> transformMyChannelRecord: 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 transformMyChannelRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
channel_id: 'cd',
last_post_at: 1617311494451,
last_viewed_at: 1617311494451,
mentions_count: 3,
message_count: 10,
roles: 'guest',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('MyChannel');
});
});

View File

@@ -0,0 +1,148 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
import Channel from '@typings/database/models/servers/channel';
import ChannelInfo from '@typings/database/models/servers/channel_info';
import {
TransformerArgs,
RawChannel,
RawChannelInfo,
RawMyChannel,
RawMyChannelSettings,
} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
import MyChannel from '@typings/database/models/servers/my_channel';
import MyChannelSettings from '@typings/database/models/servers/my_channel_settings';
const {
CHANNEL,
CHANNEL_INFO,
MY_CHANNEL,
MY_CHANNEL_SETTINGS,
} = MM_TABLES.SERVER;
/**
* transformChannelRecord: Prepares a record of the SERVER database 'Channel' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformChannelRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformMyChannelSettingsRecord: Prepares a record of the SERVER database 'MyChannelSettings' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformMyChannelSettingsRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawMyChannelSettings;
const record = value.record as MyChannelSettings;
const isCreateAction = action === OperationType.CREATE;
const fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformChannelInfoRecord: Prepares a record of the SERVER database 'ChannelInfo' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformChannelInfoRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawChannelInfo;
const record = value.record as ChannelInfo;
const isCreateAction = action === OperationType.CREATE;
const fieldsMapper = (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.pinnedPostCount = raw.pinned_post_count;
channelInfo.purpose = raw.purpose;
};
return prepareBaseRecord({
action,
database,
tableName: CHANNEL_INFO,
value,
fieldsMapper,
});
};
/**
* transformMyChannelRecord: Prepares a record of the SERVER database 'MyChannel' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformMyChannelRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawMyChannel;
const record = value.record as MyChannel;
const isCreateAction = action === OperationType.CREATE;
const fieldsMapper = (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,
fieldsMapper,
});
};

View File

@@ -0,0 +1,113 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
transformCustomEmojiRecord,
transformRoleRecord,
transformSystemRecord,
transformTermsOfServiceRecord,
} from '@database/operator/server_data_operator/transformers/general';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
describe('*** Role Prepare Records Test ***', () => {
it('=> transformRoleRecord: 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 transformRoleRecord({
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');
});
});
describe('*** System Prepare Records Test ***', () => {
it('=> transformSystemRecord: 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 transformSystemRecord({
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');
});
});
describe('*** TOS Prepare Records Test ***', () => {
it('=> transformTermsOfServiceRecord: 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 transformTermsOfServiceRecord({
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');
});
});
describe('*** CustomEmoj Prepare Records Test ***', () => {
it('=> transformCustomEmojiRecord: 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 transformCustomEmojiRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'i',
create_at: 1580913641769,
update_at: 1580913641769,
delete_at: 0,
creator_id: '4cprpki7ri81mbx8efixcsb8jo',
name: 'boomI',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('CustomEmoji');
});
});

View File

@@ -0,0 +1,131 @@
// 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/server_data_operator/transformers/index';
import CustomEmoji from '@typings/database/models/servers/custom_emoji';
import {
TransformerArgs,
RawCustomEmoji,
RawRole,
RawSystem,
RawTermsOfService,
} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
import Role from '@typings/database/models/servers/role';
import System from '@typings/database/models/servers/system';
import TermsOfService from '@typings/database/models/servers/terms_of_service';
const {
CUSTOM_EMOJI,
ROLE,
SYSTEM,
TERMS_OF_SERVICE,
} = MM_TABLES.SERVER;
/**
* transformCustomEmojiRecord: Prepares a record of the SERVER database 'CustomEmoji' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformCustomEmojiRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (emoji: CustomEmoji) => {
emoji._raw.id = isCreateAction ? (raw?.id ?? emoji.id) : record.id;
emoji.name = raw.name;
};
return prepareBaseRecord({
action,
database,
tableName: CUSTOM_EMOJI,
value,
fieldsMapper,
});
};
/**
* transformRoleRecord: Prepares a record of the SERVER database 'Role' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformRoleRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformSystemRecord: Prepares a record of the SERVER database 'System' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformSystemRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawSystem;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const fieldsMapper = (system: System) => {
system.name = raw?.name;
system.value = raw?.value;
};
return prepareBaseRecord({
action,
database,
tableName: SYSTEM,
value,
fieldsMapper,
});
};
/**
* transformTermsOfServiceRecord: Prepares a record of the SERVER database 'TermsOfService' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformTermsOfServiceRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (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,
fieldsMapper,
});
};

View File

@@ -0,0 +1,124 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
transformGroupMembershipRecord,
transformGroupRecord,
transformGroupsInChannelRecord,
transformGroupsInTeamRecord,
} from '@database/operator/server_data_operator/transformers/group';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
describe('*** GROUP Prepare Records Test ***', () => {
it('=> transformGroupRecord: 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 transformGroupRecord({
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('=> transformGroupsInTeamRecord: 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 transformGroupsInTeamRecord({
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('=> transformGroupsInChannelRecord: 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 transformGroupsInChannelRecord({
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('=> transformGroupMembershipRecord: 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 transformGroupMembershipRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
user_id: 'u4cprpki7ri81mbx8efixcsb8jo',
group_id: 'g4cprpki7ri81mbx8efixcsb8jo',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('GroupMembership');
});
});

View File

@@ -0,0 +1,136 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
import {
TransformerArgs,
RawGroup,
RawGroupMembership,
RawGroupsInChannel,
RawGroupsInTeam,
} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
import Group from '@typings/database/models/servers/group';
import GroupMembership from '@typings/database/models/servers/group_membership';
import GroupsInChannel from '@typings/database/models/servers/groups_in_channel';
import GroupsInTeam from '@typings/database/models/servers/groups_in_team';
const {
GROUP,
GROUPS_IN_CHANNEL,
GROUPS_IN_TEAM,
GROUP_MEMBERSHIP,
} = MM_TABLES.SERVER;
/**
* transformGroupMembershipRecord: Prepares a record of the SERVER database 'GroupMembership' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformGroupMembershipRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformGroupRecord: Prepares a record of the SERVER database 'Group' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformGroupRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformGroupsInTeamRecord: Prepares a record of the SERVER database 'GroupsInTeam' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformGroupsInTeamRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawGroupsInTeam;
const record = value.record as GroupsInTeam;
const isCreateAction = action === OperationType.CREATE;
const fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformGroupsInChannelRecord: Prepares a record of the SERVER database 'GroupsInChannel' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformGroupsInChannelRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawGroupsInChannel;
const record = value.record as GroupsInChannel;
const isCreateAction = action === OperationType.CREATE;
const fieldsMapper = (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,
fieldsMapper,
});
};

View File

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

View File

@@ -0,0 +1,185 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
transformDraftRecord,
transformFileRecord,
transformPostInThreadRecord,
transformPostMetadataRecord,
transformPostRecord,
transformPostsInChannelRecord,
} from '@database/operator/server_data_operator/transformers/post';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
describe('*** POST Prepare Records Test ***', () => {
it('=> transformPostRecord: 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 transformPostRecord({
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 composer 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('=> transformPostInThreadRecord: 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 transformPostInThreadRecord({
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('=> transformFileRecord: 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 transformFileRecord({
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('=> transformPostMetadataRecord: 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 transformPostMetadataRecord({
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('=> transformDraftRecord: 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 transformDraftRecord({
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('=> transformPostsInChannelRecord: 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 transformPostsInChannelRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'ps81i4yypoo',
channel_id: 'channel_idp23232e',
earliest: 1608253011321,
latest: 1609253011321,
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe(
'PostsInChannel',
);
});
});

View File

@@ -0,0 +1,218 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Q} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
import type{
TransformerArgs,
RawDraft,
RawFile,
RawPost,
RawPostMetadata,
RawPostsInChannel,
RawPostsInThread,
} from '@typings/database/database';
import Draft from '@typings/database/models/servers/draft';
import {OperationType} from '@typings/database/enums';
import File from '@typings/database/models/servers/file';
import Post from '@typings/database/models/servers/post';
import PostMetadata from '@typings/database/models/servers/post_metadata';
import PostsInChannel from '@typings/database/models/servers/posts_in_channel';
import PostsInThread from '@typings/database/models/servers/posts_in_thread';
const {
DRAFT,
FILE,
POST,
POSTS_IN_CHANNEL,
POSTS_IN_THREAD,
POST_METADATA,
} = MM_TABLES.SERVER;
/**
* transformPostRecord: Prepares a record of the SERVER database 'Post' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformPostRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformPostInThreadRecord: Prepares a record of the SERVER database 'PostsInThread' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformPostInThreadRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawPostsInThread;
const record = value.record as PostsInThread;
const isCreateAction = action === OperationType.CREATE;
const fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformFileRecord: Prepares a record of the SERVER database 'Files' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformFileRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformPostMetadataRecord: Prepares a record of the SERVER database 'PostMetadata' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformPostMetadataRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawPostMetadata;
const record = value.record as PostMetadata;
const isCreateAction = action === OperationType.CREATE;
const fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformDraftRecord: Prepares a record of the SERVER database 'Draft' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformDraftRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformPostsInChannelRecord: Prepares a record of the SERVER database 'PostsInChannel' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformPostsInChannelRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawPostsInChannel;
const record = value.record as PostsInChannel;
const isCreateAction = action === OperationType.CREATE;
const fieldsMapper = (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,
fieldsMapper,
});
};

View File

@@ -0,0 +1,186 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
transformMyTeamRecord,
transformSlashCommandRecord,
transformTeamChannelHistoryRecord,
transformTeamMembershipRecord,
transformTeamRecord,
transformTeamSearchHistoryRecord,
} from '@database/operator/server_data_operator/transformers/team';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
describe('*** TEAM Prepare Records Test ***', () => {
it('=> transformSlashCommandRecord: 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 transformSlashCommandRecord({
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('=> transformMyTeamRecord: 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 transformMyTeamRecord({
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('=> transformTeamRecord: 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 transformTeamRecord({
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('=> transformTeamChannelHistoryRecord: 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 transformTeamChannelHistoryRecord({
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('=> transformTeamSearchHistoryRecord: 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 transformTeamSearchHistoryRecord({
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('=> transformTeamMembershipRecord: 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 transformTeamMembershipRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
team_id: 'a',
user_id: 'ab',
roles: '3ngdqe1e7tfcbmam4qgnxp91bw',
delete_at: 0,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('TeamMembership');
});
});

View File

@@ -0,0 +1,214 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
import type {
TransformerArgs,
RawMyTeam,
RawSlashCommand,
RawTeam,
RawTeamChannelHistory,
RawTeamMembership,
RawTeamSearchHistory,
} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
import MyTeam from '@typings/database/models/servers/my_team';
import SlashCommand from '@typings/database/models/servers/slash_command';
import Team from '@typings/database/models/servers/team';
import TeamChannelHistory from '@typings/database/models/servers/team_channel_history';
import TeamMembership from '@typings/database/models/servers/team_membership';
import TeamSearchHistory from '@typings/database/models/servers/team_search_history';
const {
MY_TEAM,
SLASH_COMMAND,
TEAM,
TEAM_CHANNEL_HISTORY,
TEAM_MEMBERSHIP,
TEAM_SEARCH_HISTORY,
} = MM_TABLES.SERVER;
/**
* transformTeamMembershipRecord: Prepares a record of the SERVER database 'TeamMembership' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformTeamMembershipRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformTeamRecord: Prepares a record of the SERVER database 'Team' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformTeamRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformTeamChannelHistoryRecord: Prepares a record of the SERVER database 'TeamChannelHistory' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformTeamChannelHistoryRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawTeamChannelHistory;
const record = value.record as TeamChannelHistory;
const isCreateAction = action === OperationType.CREATE;
const fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformTeamSearchHistoryRecord: Prepares a record of the SERVER database 'TeamSearchHistory' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformTeamSearchHistoryRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawTeamSearchHistory;
const record = value.record as TeamSearchHistory;
const isCreateAction = action === OperationType.CREATE;
const fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformSlashCommandRecord: Prepares a record of the SERVER database 'SlashCommand' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformSlashCommandRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformMyTeamRecord: Prepares a record of the SERVER database 'MyTeam' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformMyTeamRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawMyTeam;
const record = value.record as MyTeam;
const isCreateAction = action === OperationType.CREATE;
const fieldsMapper = (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,
fieldsMapper,
});
};

View File

@@ -0,0 +1,155 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
transformChannelMembershipRecord,
transformPreferenceRecord,
transformReactionRecord,
transformUserRecord,
} from '@database/operator/server_data_operator/transformers/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('=> transformChannelMembershipRecord: should return an array of type ChannelMembership', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'user_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await transformChannelMembershipRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
channel_id: '17bfnb1uwb8epewp4q3x3rx9go',
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
roles: 'wqyby5r5pinxxdqhoaomtacdhc',
last_viewed_at: 1613667352029,
msg_count: 3864,
mention_count: 0,
notify_props: {
desktop: 'default',
email: 'default',
ignore_channel_mentions: 'default',
mark_unread: 'mention',
push: 'default',
},
last_update_at: 1613667352029,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('ChannelMembership');
});
it('=> transformPreferenceRecord: 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 transformPreferenceRecord({
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('=> transformReactionRecord: 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 transformReactionRecord({
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('=> transformUserRecord: 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 transformUserRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: '9ciscaqbrpd6d8s68k76xb9bte',
is_bot: false,
create_at: 1599457495881,
update_at: 1607683720173,
delete_at: 0,
username: 'a.l',
auth_service: 'saml',
email: 'a.l@mattermost.com',
email_verified: true,
nickname: '',
first_name: 'A',
last_name: 'L',
position: 'Mobile Engineer',
roles: 'system_user',
props: {},
notify_props: {
desktop: 'all',
desktop_sound: true,
email: true,
first_name: true,
mention_keys: '',
push: 'mention',
channel: true,
auto_responder_active: false,
auto_responder_message: 'Hello, I am out of office and unable to respond to messages.',
comments: 'never',
desktop_notification_sound: 'Hello',
push_status: 'online',
},
last_password_update: 1604323112537,
last_picture_update: 1604686302260,
locale: 'en',
timezone: {
automaticTimezone: 'Indian/Mauritius',
manualTimezone: '',
useAutomaticTimezone: 'true',
},
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('User');
});
});

View File

@@ -0,0 +1,149 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
import ChannelMembership from '@typings/database/models/servers/channel_membership';
import {TransformerArgs, RawChannelMembership, RawPreference, RawReaction, RawUser} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
import Preference from '@typings/database/models/servers/preference';
import Reaction from '@typings/database/models/servers/reaction';
import User from '@typings/database/models/servers/user';
const {
CHANNEL_MEMBERSHIP,
PREFERENCE,
REACTION,
USER,
} = MM_TABLES.SERVER;
/**
* transformReactionRecord: Prepares a record of the SERVER database 'Reaction' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformReactionRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformUserRecord: Prepares a record of the SERVER database 'User' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformUserRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (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,
fieldsMapper,
});
};
/**
* transformPreferenceRecord: Prepares a record of the SERVER database 'Preference' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformPreferenceRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (preference: Preference) => {
preference._raw.id = isCreateAction ? 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,
fieldsMapper,
});
};
/**
* transformChannelMembershipRecord: Prepares a record of the SERVER database 'ChannelMembership' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformChannelMembershipRecord = ({action, database, value}: TransformerArgs) => {
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 fieldsMapper = (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,
fieldsMapper,
});
};