diff --git a/app/actions/remote/user.ts b/app/actions/remote/user.ts index 3b7558b0ee..d36b3f780e 100644 --- a/app/actions/remote/user.ts +++ b/app/actions/remote/user.ts @@ -190,7 +190,7 @@ export const loadMe = async (serverUrl: string, {deviceToken, user}: LoadMeArgs) const myTeams = teamUnreads.map((unread) => { const matchingTeam = teamMemberships.find((team) => team.team_id === unread.team_id); - return {team_id: unread.team_id, roles: matchingTeam?.roles ?? '', is_unread: unread.msg_count > 0, mentions_count: unread.mention_count}; + return {id: unread.team_id, roles: matchingTeam?.roles ?? '', is_unread: unread.msg_count > 0, mentions_count: unread.mention_count}; }); const myTeamRecords = operator.handleMyTeam({ diff --git a/app/constants/action_type.ts b/app/constants/action_type.ts new file mode 100644 index 0000000000..7193a62a4d --- /dev/null +++ b/app/constants/action_type.ts @@ -0,0 +1,16 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import keyMirror from '@utils/key_mirror'; + +export const POSTS = keyMirror({ + RECEIVED_IN_CHANNEL: null, + RECEIVED_SINCE: null, + RECEIVED_AFTER: null, + RECEIVED_BEFORE: null, + RECEIVED_NEW: null, +}); + +export default { + POSTS, +}; diff --git a/app/constants/general.ts b/app/constants/general.ts index a9dedcd0fa..7e9dc21a5a 100644 --- a/app/constants/general.ts +++ b/app/constants/general.ts @@ -9,6 +9,7 @@ export default { LOGS_PAGE_SIZE_DEFAULT: 10000, PROFILE_CHUNK_SIZE: 100, CHANNELS_CHUNK_SIZE: 50, + POST_CHUNK_SIZE: 60, TEAMS_CHUNK_SIZE: 50, SEARCH_TIMEOUT_MILLISECONDS: 100, STATUS_INTERVAL: 60000, diff --git a/app/constants/index.ts b/app/constants/index.ts index 170f75ac62..7a005e4845 100644 --- a/app/constants/index.ts +++ b/app/constants/index.ts @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import ActionType from './action_type'; import Attachment from './attachment'; import Database from './database'; import Device from './device'; @@ -9,6 +10,7 @@ import General from './general'; import List from './list'; import Navigation from './navigation'; import Network from './network'; +import Permissions from './permissions'; import Preferences from './preferences'; import Screens from './screens'; import SSO, {REDIRECT_URL_SCHEME, REDIRECT_URL_SCHEME_DEV} from './sso'; @@ -16,6 +18,7 @@ import View, {Upgrade} from './view'; import WebsocketEvents from './websocket'; export { + ActionType, Attachment, Database, Device, @@ -24,6 +27,7 @@ export { List, Navigation, Network, + Permissions, Preferences, REDIRECT_URL_SCHEME, REDIRECT_URL_SCHEME_DEV, diff --git a/app/constants/permissions.ts b/app/constants/permissions.ts new file mode 100644 index 0000000000..c42119734f --- /dev/null +++ b/app/constants/permissions.ts @@ -0,0 +1,84 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export default { + INVITE_USER: 'invite_user', + ADD_USER_TO_TEAM: 'add_user_to_team', + USE_SLASH_COMMANDS: 'use_slash_commands', + MANAGE_SLASH_COMMANDS: 'manage_slash_commands', + MANAGE_OTHERS_SLASH_COMMANDS: 'manage_others_slash_commands', + CREATE_PUBLIC_CHANNEL: 'create_public_channel', + CREATE_PRIVATE_CHANNEL: 'create_private_channel', + MANAGE_PUBLIC_CHANNEL_MEMBERS: 'manage_public_channel_members', + MANAGE_PRIVATE_CHANNEL_MEMBERS: 'manage_private_channel_members', + ASSIGN_SYSTEM_ADMIN_ROLE: 'assign_system_admin_role', + MANAGE_ROLES: 'manage_roles', + MANAGE_TEAM_ROLES: 'manage_team_roles', + MANAGE_CHANNEL_ROLES: 'manage_channel_roles', + MANAGE_SYSTEM: 'manage_system', + CREATE_DIRECT_CHANNEL: 'create_direct_channel', + CREATE_GROUP_CHANNEL: 'create_group_channel', + MANAGE_PUBLIC_CHANNEL_PROPERTIES: 'manage_public_channel_properties', + MANAGE_PRIVATE_CHANNEL_PROPERTIES: 'manage_private_channel_properties', + LIST_PUBLIC_TEAMS: 'list_public_teams', + JOIN_PUBLIC_TEAMS: 'join_public_teams', + LIST_PRIVATE_TEAMS: 'list_private_teams', + JOIN_PRIVATE_TEAMS: 'join_private_teams', + LIST_TEAM_CHANNELS: 'list_team_channels', + JOIN_PUBLIC_CHANNELS: 'join_public_channels', + DELETE_PUBLIC_CHANNEL: 'delete_public_channel', + CONVERT_PUBLIC_CHANNEL_TO_PRIVATE: 'convert_public_channel_to_private', + DELETE_PRIVATE_CHANNEL: 'delete_private_channel', + EDIT_OTHER_USERS: 'edit_other_users', + READ_CHANNEL: 'read_channel', + READ_PUBLIC_CHANNEL: 'read_public_channel', + ADD_REACTION: 'add_reaction', + REMOVE_REACTION: 'remove_reaction', + REMOVE_OTHERS_REACTIONS: 'remove_others_reactions', + PERMANENT_DELETE_USER: 'permanent_delete_user', + UPLOAD_FILE: 'upload_file', + GET_PUBLIC_LINK: 'get_public_link', + MANAGE_WEBHOOKS: 'manage_webhooks', + MANAGE_OTHERS_WEBHOOKS: 'manage_others_webhooks', + MANAGE_INCOMING_WEBHOOKS: 'manage_incoming_webhooks', + MANAGE_OTHERS_INCOMING_WEBHOOKS: 'manage_others_incoming_webhooks', + MANAGE_OUTGOING_WEBHOOKS: 'manage_outgoing_webhooks', + MANAGE_OTHERS_OUTGOING_WEBHOOKS: 'manage_others_outgoing_webhooks', + MANAGE_OAUTH: 'manage_oauth', + MANAGE_SYSTEM_WIDE_OAUTH: 'manage_system_wide_oauth', + CREATE_POST: 'create_post', + CREATE_POST_PUBLIC: 'create_post_public', + EDIT_POST: 'edit_post', + EDIT_OTHERS_POSTS: 'edit_others_posts', + DELETE_POST: 'delete_post', + DELETE_OTHERS_POSTS: 'delete_others_posts', + REMOVE_USER_FROM_TEAM: 'remove_user_from_team', + CREATE_TEAM: 'create_team', + MANAGE_TEAM: 'manage_team', + IMPORT_TEAM: 'import_team', + VIEW_TEAM: 'view_team', + LIST_USERS_WITHOUT_TEAM: 'list_users_without_team', + CREATE_USER_ACCESS_TOKEN: 'create_user_access_token', + READ_USER_ACCESS_TOKEN: 'read_user_access_token', + REVOKE_USER_ACCESS_TOKEN: 'revoke_user_access_token', + MANAGE_JOBS: 'manage_jobs', + MANAGE_EMOJIS: 'manage_emojis', + MANAGE_OTHERS_EMOJIS: 'manage_others_emojis', + CREATE_EMOJIS: 'create_emojis', + DELETE_EMOJIS: 'delete_emojis', + DELETE_OTHERS_EMOJIS: 'delete_others_emojis', + VIEW_MEMBERS: 'view_members', + INVITE_GUEST: 'invite_guest', + PROMOTE_GUEST: 'promote_guest', + DEMOTE_TO_GUEST: 'demote_to_guest', + USE_CHANNEL_MENTIONS: 'use_channel_mentions', + USE_GROUP_MENTIONS: 'use_group_mentions', + + CHANNEL_MODERATED_PERMISSIONS: { + CREATE_POST: 'create_post', + CREATE_REACTIONS: 'create_reactions', + MANAGE_MEMBERS: 'manage_members', + USE_CHANNEL_MENTIONS: 'use_channel_mentions', + }, + MANAGE_BOTS: 'manage_bots', + MANAGE_OTHERS_BOTS: 'manage_others_bots', +}; diff --git a/app/constants/view.ts b/app/constants/view.ts index 7711ba5b66..fe1e86c759 100644 --- a/app/constants/view.ts +++ b/app/constants/view.ts @@ -1,7 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Platform} from 'react-native'; import DeviceInfo from 'react-native-device-info'; import keyMirror from '@utils/key_mirror'; @@ -111,7 +110,6 @@ const RequiredServer = { export default { ...ViewTypes, RequiredServer, - POST_VISIBILITY_CHUNK_SIZE: Platform.OS === 'android' ? 15 : 60, FEATURE_TOGGLE_PREFIX: 'feature_enabled_', EMBED_PREVIEW: 'embed_preview', LINK_PREVIEW_DISPLAY: 'link_previews', diff --git a/app/database/models/server/channel.ts b/app/database/models/server/channel.ts index 42a7fd0058..8eb483589c 100644 --- a/app/database/models/server/channel.ts +++ b/app/database/models/server/channel.ts @@ -61,7 +61,7 @@ export default class ChannelModel extends Model { [MY_CHANNEL_SETTINGS]: {type: 'has_many', foreignKey: 'channel_id'}, /** A CHANNEL can be associated with multiple POSTS_IN_CHANNEL (relationship is 1:N) */ - [POSTS_IN_CHANNEL]: {type: 'has_many', foreignKey: 'channel_id'}, + [POSTS_IN_CHANNEL]: {type: 'has_many', foreignKey: 'id'}, /** A CHANNEL can contain multiple POST (relationship is 1:N) */ [POST]: {type: 'has_many', foreignKey: 'channel_id'}, diff --git a/app/database/models/server/channel_info.ts b/app/database/models/server/channel_info.ts index a0ed799425..54441c9c6a 100644 --- a/app/database/models/server/channel_info.ts +++ b/app/database/models/server/channel_info.ts @@ -24,12 +24,9 @@ export default class ChannelInfoModel extends Model { static associations: Associations = { /** A CHANNEL is associated with only one CHANNEL_INFO (relationship is 1:1) */ - [CHANNEL]: {type: 'belongs_to', key: 'channel_id'}, + [CHANNEL]: {type: 'belongs_to', key: 'id'}, }; - /** channel_id : The foreign key from CHANNEL */ - @field('channel_id') channelId!: string; - /** guest_count : The number of guest in this channel */ @field('guest_count') guestCount!: number; @@ -46,5 +43,5 @@ export default class ChannelInfoModel extends Model { @field('purpose') purpose!: string; /** channel : The lazy query property to the record from CHANNEL table */ - @immutableRelation(CHANNEL, 'channel_id') channel!: Relation; + @immutableRelation(CHANNEL, 'id') channel!: Relation; } diff --git a/app/database/models/server/my_channel.ts b/app/database/models/server/my_channel.ts index 5384f6f297..5c266cf861 100644 --- a/app/database/models/server/my_channel.ts +++ b/app/database/models/server/my_channel.ts @@ -22,12 +22,9 @@ export default class MyChannelModel extends Model { static associations: Associations = { /** A CHANNEL can be associated to only one record from the MY_CHANNEL table (relationship is 1:1) */ - [CHANNEL]: {type: 'belongs_to', key: 'channel_id'}, + [CHANNEL]: {type: 'belongs_to', key: 'id'}, }; - /** channel_id : The foreign key to the related Channel record */ - @field('channel_id') channelId!: string; - /** last_post_at : The timestamp for any last post on this channel */ @field('last_post_at') lastPostAt!: number; @@ -44,5 +41,5 @@ export default class MyChannelModel extends Model { @field('roles') roles!: string; /** channel : The relation pointing to the CHANNEL table */ - @immutableRelation(CHANNEL, 'channel_id') channel!: Relation; + @immutableRelation(CHANNEL, 'id') channel!: Relation; } diff --git a/app/database/models/server/my_channel_settings.ts b/app/database/models/server/my_channel_settings.ts index df92a95656..e2c15d7a61 100644 --- a/app/database/models/server/my_channel_settings.ts +++ b/app/database/models/server/my_channel_settings.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {Relation} from '@nozbe/watermelondb'; -import {field, immutableRelation, json} from '@nozbe/watermelondb/decorators'; +import {immutableRelation, json} from '@nozbe/watermelondb/decorators'; import Model, {Associations} from '@nozbe/watermelondb/Model'; import {MM_TABLES} from '@constants/database'; @@ -24,15 +24,12 @@ export default class MyChannelSettingsModel extends Model { static associations: Associations = { /** A CHANNEL is related to only one MY_CHANNEL_SETTINGS (relationship is 1:1) */ - [CHANNEL]: {type: 'belongs_to', key: 'channel_id'}, + [CHANNEL]: {type: 'belongs_to', key: 'id'}, }; - /** channel_id : The foreign key to the related CHANNEL record */ - @field('channel_id') channelId!: string; - /** notify_props : Configurations with regards to this channel */ @json('notify_props', safeParseJSON) notifyProps!: ChannelNotifyProps; /** channel : The relation pointing to the CHANNEL table */ - @immutableRelation(CHANNEL, 'channel_id') channel!: Relation; + @immutableRelation(CHANNEL, 'id') channel!: Relation; } diff --git a/app/database/models/server/my_team.ts b/app/database/models/server/my_team.ts index 2073303e8e..189d51aed6 100644 --- a/app/database/models/server/my_team.ts +++ b/app/database/models/server/my_team.ts @@ -22,7 +22,7 @@ export default class MyTeamModel extends Model { static associations: Associations = { /** TEAM and MY_TEAM have a 1:1 relationship. */ - [TEAM]: {type: 'belongs_to', key: 'team_id'}, + [TEAM]: {type: 'belongs_to', key: 'id'}, }; /** is_unread : Boolean flag for unread messages on team level */ @@ -34,9 +34,6 @@ export default class MyTeamModel extends Model { /** roles : The different permissions that this user has in the team, concatenated together with comma to form a single string. */ @field('roles') roles!: string; - /** team_id : The foreign key of the 'parent' Team record */ - @field('team_id') teamId!: string; - /** team : The relation to the TEAM, that this user belongs to */ - @relation(MY_TEAM, 'team_id') team!: Relation; + @relation(MY_TEAM, 'id') team!: Relation; } diff --git a/app/database/models/server/post.ts b/app/database/models/server/post.ts index a8d8cb67d3..42fd09d0a2 100644 --- a/app/database/models/server/post.ts +++ b/app/database/models/server/post.ts @@ -38,10 +38,10 @@ export default class PostModel extends Model { [FILE]: {type: 'has_many', foreignKey: 'post_id'}, /** A POST can have multiple POSTS_IN_THREAD. (relationship is 1:N)*/ - [POSTS_IN_THREAD]: {type: 'has_many', foreignKey: 'post_id'}, + [POSTS_IN_THREAD]: {type: 'has_many', foreignKey: 'id'}, /** A POST can have POST_METADATA. (relationship is 1:1)*/ - [POST_METADATA]: {type: 'has_many', foreignKey: 'post_id'}, + [POST_METADATA]: {type: 'has_many', foreignKey: 'id'}, /** A POST can have multiple REACTION. (relationship is 1:N)*/ [REACTION]: {type: 'has_many', foreignKey: 'post_id'}, diff --git a/app/database/models/server/post_metadata.ts b/app/database/models/server/post_metadata.ts index 163890e764..3fdef0be45 100644 --- a/app/database/models/server/post_metadata.ts +++ b/app/database/models/server/post_metadata.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {Relation} from '@nozbe/watermelondb'; -import {field, immutableRelation, json} from '@nozbe/watermelondb/decorators'; +import {immutableRelation, json} from '@nozbe/watermelondb/decorators'; import Model, {Associations} from '@nozbe/watermelondb/Model'; import {MM_TABLES} from '@constants/database'; @@ -23,15 +23,12 @@ export default class PostMetadataModel extends Model { static associations: Associations = { /** A POST can have multiple POST_METADATA.(relationship is 1:N)*/ - [POST]: {type: 'belongs_to', key: 'post_id'}, + [POST]: {type: 'belongs_to', key: 'id'}, }; - /** post_id : The foreign key of the parent POST model */ - @field('post_id') postId!: string; - /** data : Different types of data ranging from embeds to images. */ @json('data', safeParseJSON) data!: PostMetadata; /** post: The record representing the POST parent. */ - @immutableRelation(POST, 'post_id') post!: Relation; + @immutableRelation(POST, 'id') post!: Relation; } diff --git a/app/database/models/server/posts_in_channel.ts b/app/database/models/server/posts_in_channel.ts index e6c36e5282..1d1e80df29 100644 --- a/app/database/models/server/posts_in_channel.ts +++ b/app/database/models/server/posts_in_channel.ts @@ -23,12 +23,9 @@ export default class PostsInChannelModel extends Model { static associations: Associations = { /** A CHANNEL can have multiple POSTS_IN_CHANNEL. (relationship is 1:N)*/ - [CHANNEL]: {type: 'belongs_to', key: 'channel_id'}, + [CHANNEL]: {type: 'belongs_to', key: 'id'}, }; - /** channel_id : The foreign key of the related parent channel */ - @field('channel_id') channelId!: string; - /** earliest : The earliest timestamp of the post in that channel */ @field('earliest') earliest!: number; @@ -36,5 +33,5 @@ export default class PostsInChannelModel extends Model { @field('latest') latest!: number; /** channel : The parent record of the channel for those posts */ - @immutableRelation(CHANNEL, 'channel_id') channel!: Relation; + @immutableRelation(CHANNEL, 'id') channel!: Relation; } diff --git a/app/database/models/server/posts_in_thread.ts b/app/database/models/server/posts_in_thread.ts index df62bb98cb..6b8be343e0 100644 --- a/app/database/models/server/posts_in_thread.ts +++ b/app/database/models/server/posts_in_thread.ts @@ -22,8 +22,8 @@ export default class PostsInThreadModel extends Model { /** associations : Describes every relationship to this table. */ static associations: Associations = { - /** A POST can have multiple POSTS_IN_THREAD.(relationship is 1:N)*/ - [POST]: {type: 'belongs_to', key: 'post_id'}, + /** A POST can have a POSTS_IN_THREAD.(relationship is 1:1)*/ + [POST]: {type: 'belongs_to', key: 'id'}, }; /** earliest : Lower bound of a timestamp range */ @@ -32,9 +32,6 @@ export default class PostsInThreadModel extends Model { /** latest : Upper bound of a timestamp range */ @field('latest') latest!: number; - /** post_id : The foreign key of the related Post model */ - @field('post_id') postId!: string; - /** post : The related record to the parent Post model */ - @immutableRelation(POST, 'post_id') post!: Relation; + @immutableRelation(POST, 'id') post!: Relation; } diff --git a/app/database/models/server/team_channel_history.ts b/app/database/models/server/team_channel_history.ts index 2a56a7be13..e48d564389 100644 --- a/app/database/models/server/team_channel_history.ts +++ b/app/database/models/server/team_channel_history.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {Relation} from '@nozbe/watermelondb'; -import {field, immutableRelation, json} from '@nozbe/watermelondb/decorators'; +import {immutableRelation, json} from '@nozbe/watermelondb/decorators'; import Model, {Associations} from '@nozbe/watermelondb/Model'; import {MM_TABLES} from '@constants/database'; @@ -24,15 +24,12 @@ export default class TeamChannelHistoryModel extends Model { static associations: Associations = { /** A TEAM and TEAM_CHANNEL_HISTORY share a 1:1 relationship */ - [TEAM]: {type: 'belongs_to', key: 'team_id'}, + [TEAM]: {type: 'belongs_to', key: 'id'}, }; - /** team_id : The foreign key to the related Team record */ - @field('team_id') teamId!: string; - /** channel_ids : An array containing the last 5 channels visited within this team order by recency */ @json('channel_ids', safeParseJSON) channelIds!: string[]; /** team : The related record from the parent Team model */ - @immutableRelation(TEAM, 'team_id') team!: Relation; + @immutableRelation(TEAM, 'id') team!: Relation; } diff --git a/app/database/operator/base_data_operator/index.ts b/app/database/operator/base_data_operator/index.ts index 73be1f29b9..b4340f78ec 100644 --- a/app/database/operator/base_data_operator/index.ts +++ b/app/database/operator/base_data_operator/index.ts @@ -46,7 +46,7 @@ export default class BaseDataOperator { * @param {(existing: Model, newElement: RawValue) => boolean} inputsArg.findMatchingRecordBy * @returns {Promise<{ProcessRecordResults}>} */ - processRecords = async ({createOrUpdateRawValues, deleteRawValues = [], tableName, findMatchingRecordBy, fieldName}: ProcessRecordsArgs): Promise => { + processRecords = async ({createOrUpdateRawValues = [], deleteRawValues = [], tableName, findMatchingRecordBy, fieldName}: ProcessRecordsArgs): Promise => { const getRecords = async (rawValues: RawValue[]) => { // We will query a table where one of its fields can match a range of values. Hence, here we are extracting all those potential values. const columnValues: string[] = getRangeOfValues({fieldName, raws: rawValues}); diff --git a/app/database/operator/server_data_operator/comparators/index.ts b/app/database/operator/server_data_operator/comparators/index.ts index 379320ba0f..a7734821cb 100644 --- a/app/database/operator/server_data_operator/comparators/index.ts +++ b/app/database/operator/server_data_operator/comparators/index.ts @@ -6,6 +6,7 @@ import type ChannelInfoModel from '@typings/database/models/servers/channel_info import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership'; import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji'; import type DraftModel from '@typings/database/models/servers/draft'; +import type FileModel from '@typings/database/models/servers/file'; import type GroupModel from '@typings/database/models/servers/group'; import type GroupMembershipModel from '@typings/database/models/servers/group_membership'; import type GroupsInChannelModel from '@typings/database/models/servers/groups_in_channel'; @@ -14,6 +15,7 @@ import type MyChannelModel from '@typings/database/models/servers/my_channel'; import type MyChannelSettingsModel from '@typings/database/models/servers/my_channel_settings'; import type MyTeamModel from '@typings/database/models/servers/my_team'; import type PostModel from '@typings/database/models/servers/post'; +import type PostMetadataModel from '@typings/database/models/servers/post_metadata'; import type PreferenceModel from '@typings/database/models/servers/preference'; import type RoleModel from '@typings/database/models/servers/role'; import type SlashCommandModel from '@typings/database/models/servers/slash_command'; @@ -97,7 +99,7 @@ export const isRecordTeamEqualToRaw = (record: TeamModel, raw: Team) => { }; export const isRecordTeamChannelHistoryEqualToRaw = (record: TeamChannelHistoryModel, raw: TeamChannelHistory) => { - return raw.team_id === record.teamId; + return raw.id === record.id; }; export const isRecordTeamSearchHistoryEqualToRaw = (record: TeamSearchHistoryModel, raw: TeamSearchHistory) => { @@ -109,7 +111,7 @@ export const isRecordSlashCommandEqualToRaw = (record: SlashCommandModel, raw: S }; export const isRecordMyTeamEqualToRaw = (record: MyTeamModel, raw: MyTeam) => { - return raw.team_id === record.teamId; + return raw.id === record.id; }; export const isRecordChannelEqualToRaw = (record: ChannelModel, raw: Channel) => { @@ -117,13 +119,21 @@ export const isRecordChannelEqualToRaw = (record: ChannelModel, raw: Channel) => }; export const isRecordMyChannelSettingsEqualToRaw = (record: MyChannelSettingsModel, raw: ChannelMembership) => { - return raw.channel_id === record.channelId; + return raw.channel_id === record.id; }; export const isRecordChannelInfoEqualToRaw = (record: ChannelInfoModel, raw: ChannelInfo) => { - return raw.channel_id === record.channelId; + return raw.id === record.id; }; export const isRecordMyChannelEqualToRaw = (record: MyChannelModel, raw: ChannelMembership) => { - return raw.channel_id === record.channelId; + return raw.channel_id === record.id; +}; + +export const isRecordFileEqualToRaw = (record: FileModel, raw: FileInfo) => { + return raw.id === record.id; +}; + +export const isRecordMetadataEqualToRaw = (record: PostMetadataModel, raw: Metadata) => { + return raw.id === record.id; }; diff --git a/app/database/operator/server_data_operator/handlers/channel.test.ts b/app/database/operator/server_data_operator/handlers/channel.test.ts index 23872a4322..47852f7603 100644 --- a/app/database/operator/server_data_operator/handlers/channel.test.ts +++ b/app/database/operator/server_data_operator/handlers/channel.test.ts @@ -72,6 +72,7 @@ describe('*** Operator: Channel Handlers tests ***', () => { const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords'); const settings: ChannelMembership[] = [ { + id: 'c', user_id: 'me', channel_id: 'c', roles: '', @@ -96,7 +97,7 @@ describe('*** Operator: Channel Handlers tests ***', () => { expect(spyOnHandleRecords).toHaveBeenCalledTimes(1); expect(spyOnHandleRecords).toHaveBeenCalledWith({ - fieldName: 'channel_id', + fieldName: 'id', createOrUpdateRawValues: settings, tableName: 'MyChannelSettings', prepareRecordsOnly: false, @@ -111,7 +112,7 @@ describe('*** Operator: Channel Handlers tests ***', () => { const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords'); const channelInfos = [ { - channel_id: 'c', + id: 'c', guest_count: 10, header: 'channel info header', member_count: 10, @@ -128,7 +129,7 @@ describe('*** Operator: Channel Handlers tests ***', () => { expect(spyOnHandleRecords).toHaveBeenCalledTimes(1); expect(spyOnHandleRecords).toHaveBeenCalledWith({ - fieldName: 'channel_id', + fieldName: 'id', createOrUpdateRawValues: channelInfos, tableName: 'ChannelInfo', prepareRecordsOnly: false, @@ -143,6 +144,7 @@ describe('*** Operator: Channel Handlers tests ***', () => { const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords'); const myChannels: ChannelMembership[] = [ { + id: 'c', user_id: 'me', channel_id: 'c', last_post_at: 1617311494451, @@ -168,7 +170,7 @@ describe('*** Operator: Channel Handlers tests ***', () => { expect(spyOnHandleRecords).toHaveBeenCalledTimes(1); expect(spyOnHandleRecords).toHaveBeenCalledWith({ - fieldName: 'channel_id', + fieldName: 'id', createOrUpdateRawValues: myChannels, tableName: 'MyChannel', prepareRecordsOnly: false, diff --git a/app/database/operator/server_data_operator/handlers/channel.ts b/app/database/operator/server_data_operator/handlers/channel.ts index 99665055c4..81d90aeee7 100644 --- a/app/database/operator/server_data_operator/handlers/channel.ts +++ b/app/database/operator/server_data_operator/handlers/channel.ts @@ -80,10 +80,10 @@ const ChannelHandler = (superclass: any) => class extends superclass { ); } - const createOrUpdateRawValues = getUniqueRawsBy({raws: settings, key: 'channel_id'}); + const createOrUpdateRawValues = getUniqueRawsBy({raws: settings, key: 'id'}); return this.handleRecords({ - fieldName: 'channel_id', + fieldName: 'id', findMatchingRecordBy: isRecordMyChannelSettingsEqualToRaw, transformer: transformMyChannelSettingsRecord, prepareRecordsOnly, @@ -109,11 +109,11 @@ const ChannelHandler = (superclass: any) => class extends superclass { const createOrUpdateRawValues = getUniqueRawsBy({ raws: channelInfos, - key: 'channel_id', + key: 'id', }); return this.handleRecords({ - fieldName: 'channel_id', + fieldName: 'id', findMatchingRecordBy: isRecordChannelInfoEqualToRaw, transformer: transformChannelInfoRecord, prepareRecordsOnly, @@ -139,11 +139,11 @@ const ChannelHandler = (superclass: any) => class extends superclass { const createOrUpdateRawValues = getUniqueRawsBy({ raws: myChannels, - key: 'channel_id', + key: 'id', }); return this.handleRecords({ - fieldName: 'channel_id', + fieldName: 'id', findMatchingRecordBy: isRecordMyChannelEqualToRaw, transformer: transformMyChannelRecord, prepareRecordsOnly, diff --git a/app/database/operator/server_data_operator/handlers/index.test.ts b/app/database/operator/server_data_operator/handlers/index.test.ts index 8a99eab19b..99356fea6c 100644 --- a/app/database/operator/server_data_operator/handlers/index.test.ts +++ b/app/database/operator/server_data_operator/handlers/index.test.ts @@ -77,7 +77,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => { expect(spyOnHandleRecords).toHaveBeenCalledTimes(1); expect(spyOnHandleRecords).toHaveBeenCalledWith({ - fieldName: 'id', + fieldName: 'name', createOrUpdateRawValues: emojis, tableName: 'CustomEmoji', prepareRecordsOnly: false, diff --git a/app/database/operator/server_data_operator/handlers/index.ts b/app/database/operator/server_data_operator/handlers/index.ts index 3ac8356147..4401444209 100644 --- a/app/database/operator/server_data_operator/handlers/index.ts +++ b/app/database/operator/server_data_operator/handlers/index.ts @@ -18,6 +18,8 @@ import { } from '@database/operator/server_data_operator/transformers/general'; import {getUniqueRawsBy} from '@database/operator/utils/general'; +import type {Model} from '@nozbe/watermelondb'; + import type {HandleCustomEmojiArgs, HandleRoleArgs, HandleSystemArgs, HandleTOSArgs, OperationArgs} from '@typings/database/database'; import type RoleModel from '@typings/database/models/servers/role'; import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji'; @@ -52,11 +54,11 @@ export default class ServerDataOperatorBase extends BaseDataOperator { } return this.handleRecords({ - fieldName: 'id', + fieldName: 'name', findMatchingRecordBy: isRecordCustomEmojiEqualToRaw, transformer: transformCustomEmojiRecord, prepareRecordsOnly, - createOrUpdateRawValues: getUniqueRawsBy({raws: emojis, key: 'id'}), + createOrUpdateRawValues: getUniqueRawsBy({raws: emojis, key: 'name'}), tableName: CUSTOM_EMOJI, }) as Promise; } @@ -104,7 +106,7 @@ export default class ServerDataOperatorBase extends BaseDataOperator { * @param {(TransformerArgs) => Promise} execute.recordOperator * @returns {Promise} */ - execute = async ({createRaws, transformer, tableName, updateRaws}: OperationArgs): Promise => { + execute = async ({createRaws, transformer, tableName, updateRaws}: OperationArgs): Promise => { const models = await this.prepareRecords({ tableName, createRaws, @@ -115,5 +117,7 @@ export default class ServerDataOperatorBase extends BaseDataOperator { if (models?.length > 0) { await this.batchRecords(models); } + + return models; }; } diff --git a/app/database/operator/server_data_operator/handlers/post.test.ts b/app/database/operator/server_data_operator/handlers/post.test.ts index d4ee24bd06..2fc521142f 100644 --- a/app/database/operator/server_data_operator/handlers/post.test.ts +++ b/app/database/operator/server_data_operator/handlers/post.test.ts @@ -1,12 +1,19 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {Q} from '@nozbe/watermelondb'; + +import {ActionType} from '@constants'; 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 {createPostsChain} from '@database/operator/utils/post'; import ServerDataOperator from '..'; +Q.experimentalSortBy = jest.fn().mockImplementation((field) => { + return Q.where(field, Q.gte(0)); +}); describe('*** Operator: Post Handlers tests ***', () => { let operator: ServerDataOperator; @@ -93,7 +100,7 @@ describe('*** Operator: Post Handlers tests ***', () => { reactions: [ { user_id: 'njic1w1k5inefp848jwk6oukio', - post_id: 'a7ebyw883trm884p1qcgt8yw4a', + post_id: '8swgtrrdiff89jnsiwiip3y1eoe', emoji_name: 'clap', create_at: 1608252965442, }, @@ -207,6 +214,13 @@ describe('*** Operator: Post Handlers tests ***', () => { }, ]; + const order = [ + '8swgtrrdiff89jnsiwiip3y1eoe', + '8fcnk3p1jt8mmkaprgajoxz115a', + '3y3w3a6gkbg73bnj3xund9o5ic', + ]; + const actionType = ActionType.POSTS.RECEIVED_IN_CHANNEL; + const spyOnHandleFiles = jest.spyOn(operator, 'handleFiles'); const spyOnHandlePostMetadata = jest.spyOn(operator, 'handlePostMetadata'); const spyOnHandleReactions = jest.spyOn(operator, 'handleReactions'); @@ -216,25 +230,25 @@ describe('*** Operator: Post Handlers tests ***', () => { // handlePosts will in turn call handlePostsInThread await operator.handlePosts({ - orders: [ - '8swgtrrdiff89jnsiwiip3y1eoe', - '8fcnk3p1jt8mmkaprgajoxz115a', - '3y3w3a6gkbg73bnj3xund9o5ic', - ], - values: posts, + actionType, + order, + posts, previousPostId: '', }); expect(spyOnHandleReactions).toHaveBeenCalledTimes(1); expect(spyOnHandleReactions).toHaveBeenCalledWith({ - reactions: [ - { - user_id: 'njic1w1k5inefp848jwk6oukio', - post_id: 'a7ebyw883trm884p1qcgt8yw4a', - emoji_name: 'clap', - create_at: 1608252965442, - }, - ], + postsReactions: [{ + post_id: '8swgtrrdiff89jnsiwiip3y1eoe', + reactions: [ + { + user_id: 'njic1w1k5inefp848jwk6oukio', + post_id: '8swgtrrdiff89jnsiwiip3y1eoe', + emoji_name: 'clap', + create_at: 1608252965442, + }, + ], + }], prepareRecordsOnly: true, }); @@ -302,14 +316,14 @@ describe('*** Operator: Post Handlers tests ***', () => { }, }, }, - post_id: '8swgtrrdiff89jnsiwiip3y1eoe', + id: '8swgtrrdiff89jnsiwiip3y1eoe', }], prepareRecordsOnly: true, }); expect(spyOnHandleCustomEmojis).toHaveBeenCalledTimes(1); expect(spyOnHandleCustomEmojis).toHaveBeenCalledWith({ - prepareRecordsOnly: false, + prepareRecordsOnly: true, emojis: [ { id: 'dgwyadacdbbwjc8t357h6hwsrh', @@ -322,12 +336,19 @@ describe('*** Operator: Post Handlers tests ***', () => { ], }); + const postInThreadExpected: Record = {}; + posts.filter((p) => p.root_id).forEach((p) => { + if (postInThreadExpected[p.root_id]) { + postInThreadExpected[p.root_id].push(p); + } else { + postInThreadExpected[p.root_id] = [p]; + } + }); expect(spyOnHandlePostsInThread).toHaveBeenCalledTimes(1); - expect(spyOnHandlePostsInThread).toHaveBeenCalledWith([ - {earliest: 1596032651747, post_id: '8swgtrrdiff89jnsiwiip3y1eoe'}, - ]); + expect(spyOnHandlePostsInThread).toHaveBeenCalledWith(postInThreadExpected, ActionType.POSTS.RECEIVED_IN_CHANNEL, true); + const linkedPosts = createPostsChain({order, posts, previousPostId: ''}); expect(spyOnHandlePostsInChannel).toHaveBeenCalledTimes(1); - expect(spyOnHandlePostsInChannel).toHaveBeenCalledWith(posts.slice(0, 3)); + expect(spyOnHandlePostsInChannel).toHaveBeenCalledWith(linkedPosts.slice(0, 3), actionType, true); }); }); diff --git a/app/database/operator/server_data_operator/handlers/post.ts b/app/database/operator/server_data_operator/handlers/post.ts index d8efa67152..02e8ca5e5e 100644 --- a/app/database/operator/server_data_operator/handlers/post.ts +++ b/app/database/operator/server_data_operator/handlers/post.ts @@ -1,24 +1,21 @@ // 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 {ActionType, Database} from '@constants'; import DataOperatorException from '@database/exceptions/data_operator_exception'; -import {isRecordDraftEqualToRaw, isRecordPostEqualToRaw} from '@database/operator/server_data_operator/comparators'; +import {isRecordDraftEqualToRaw, isRecordFileEqualToRaw, isRecordMetadataEqualToRaw, 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 {getUniqueRawsBy} from '@database/operator/utils/general'; +import {createPostsChain} from '@database/operator/utils/post'; -import type {HandleDraftArgs, HandleFilesArgs, HandlePostMetadataArgs, HandlePostsArgs, RecordPair} from '@typings/database/database'; +import type {HandleDraftArgs, HandleFilesArgs, HandlePostMetadataArgs, HandlePostsArgs, ProcessRecordResults} from '@typings/database/database'; import type DraftModel from '@typings/database/models/servers/draft'; import type FileModel from '@typings/database/models/servers/file'; import type PostModel from '@typings/database/models/servers/post'; @@ -31,16 +28,14 @@ const { DRAFT, FILE, POST, - POSTS_IN_CHANNEL, - POSTS_IN_THREAD, POST_METADATA, -} = MM_TABLES.SERVER; +} = Database.MM_TABLES.SERVER; export interface PostHandlerMix { handleDraft: ({drafts, prepareRecordsOnly}: HandleDraftArgs) => Promise; handleFiles: ({files, prepareRecordsOnly}: HandleFilesArgs) => Promise; handlePostMetadata: ({metadatas, prepareRecordsOnly}: HandlePostMetadataArgs) => Promise; - handlePosts: ({orders, values, previousPostId}: HandlePostsArgs) => Promise; + handlePosts: ({actionType, order, posts, previousPostId}: HandlePostsArgs) => Promise; handlePostsInChannel: (posts: Post[]) => Promise; handlePostsInThread: (rootPosts: PostsInThread[]) => Promise; } @@ -76,158 +71,143 @@ const PostHandler = (superclass: any) => class extends superclass { /** * handlePosts: Handler responsible for the Create/Update operations occurring on the Post table from the 'Server' schema * @param {HandlePostsArgs} handlePosts + * @param {string} handlePosts.actionType * @param {string[]} handlePosts.orders * @param {RawPost[]} handlePosts.values * @param {string | undefined} handlePosts.previousPostId * @returns {Promise} */ - handlePosts = async ({orders, values, previousPostId}: HandlePostsArgs): Promise => { + handlePosts = async ({actionType, order, posts, previousPostId = ''}: HandlePostsArgs): Promise => { 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', - ); + // We rely on the posts array; if it is empty, we stop processing + if (!posts.length) { + return; } - const rawValues = getUniqueRawsBy({ - raws: values, + // Get unique posts in case they are duplicated + const uniquePosts = getUniqueRawsBy({ + raws: posts, key: 'id', }) as Post[]; - // 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, - }); + const emojis: CustomEmoji[] = []; + const files: FileInfo[] = []; + const metadatas: Metadata[] = []; + const postsReactions: ReactionsPerPost[] = []; + const pendingPostsToDelete: Post[] = []; + const postsInThread: Record = {}; - // Here we verify in our database that the postsOrdered truly need 'CREATION' - const futureEntries = await this.processRecords({ - createOrUpdateRawValues: postsOrdered, + // Let's process the post data + for (const post of posts) { + // Find any pending posts that matches the ones received to mark for deletion + if (post.pending_post_id) { + pendingPostsToDelete.push({ + ...post, + id: post.pending_post_id, + }); + } + + if (post.root_id) { + if (postsInThread[post.root_id]) { + postsInThread[post.root_id].push(post); + } else { + postsInThread[post.root_id] = [post]; + } + } + + // Process the metadata of each post + if (post?.metadata && Object.keys(post?.metadata).length > 0) { + const data = post.metadata; + + // Extracts reaction from post's metadata + if (data.reactions) { + postsReactions.push({post_id: post.id, reactions: data.reactions}); + delete data.reactions; + } + + // Extracts emojis from post's metadata + if (data.emojis) { + emojis.push(...data.emojis); + delete data.emojis; + } + + // Extracts files from post's metadata + if (data.files) { + files.push(...data.files); + delete data.files; + } + + metadatas.push({ + data, + id: post.id, + }); + } + } + + // Process the posts to get which ones need to be created and which updated + const processedPosts = (await this.processRecords({ + createOrUpdateRawValues: uniquePosts, + deleteRawValues: pendingPostsToDelete, tableName, findMatchingRecordBy: isRecordPostEqualToRaw, fieldName: 'id', - }); + })) as ProcessRecordResults; - if (futureEntries.createRaws?.length) { - let batch: Model[] = []; - const files: FileInfo[] = []; - const postsInThread = []; - const reactions: Reaction[] = []; - const emojis: CustomEmoji[] = []; - const metadatas: Metadata[] = []; + const preparedPosts = (await this.prepareRecords({ + createRaws: processedPosts.createRaws, + updateRaws: processedPosts.updateRaws, + deleteRaws: processedPosts.deleteRaws, + transformer: transformPostRecord, + tableName, + })) as PostModel[]; - // 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, - }); + // Add the models to be batched here + const batch: Model[] = [...preparedPosts]; - // Prepares records for batch processing onto the 'Post' table for the server schema - const posts = (await this.prepareRecords({ - createRaws: linkedRawPosts, - transformer: transformPostRecord, - tableName, - })) as PostModel[]; + if (postsReactions.length) { + // calls handler for Reactions + const postReactions = (await this.handleReactions({postsReactions, prepareRecordsOnly: true})) as ReactionModel[]; + batch.push(...postReactions); + } - // Appends the processed records into the final batch array - batch = batch.concat(posts); + if (files.length) { + // calls handler for Files + const postFiles = await this.handleFiles({files, prepareRecordsOnly: true}); + batch.push(...postFiles); + } - // 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 (metadatas.length) { + // calls handler for postMetadata ( embeds and images ) + const postMetadata = await this.handlePostMetadata({metadatas, prepareRecordsOnly: true}); + batch.push(...postMetadata); + } - if (post?.metadata && Object.keys(post?.metadata).length > 0) { - const data = post.metadata; + if (emojis.length) { + const postEmojis = await this.handleCustomEmojis({emojis, prepareRecordsOnly: true}); + batch.push(...postEmojis); + } - // Extracts reaction from post's metadata - if (data.reactions) { - reactions.push(...data.reactions); - delete data.reactions; - } - - // Extracts emojis from post's metadata - if (data.emojis) { - emojis.push(...data.emojis); - delete data.emojis; - } - - // Extracts files from post's metadata - if (data.files) { - files.push(...data.files); - delete data.files; - } - - metadatas.push({ - data, - post_id: post.id, - }); - } - } - - if (reactions.length) { - // calls handler for Reactions - const postReactions = (await this.handleReactions({reactions, prepareRecordsOnly: true})) as ReactionModel[]; - batch = batch.concat(postReactions); - } - - if (files.length) { - // calls handler for Files - const postFiles = await this.handleFiles({files, prepareRecordsOnly: true}); - batch = batch.concat(postFiles); - } - - if (metadatas.length) { - // calls handler for postMetadata ( embeds and images ) - const postMetadata = await this.handlePostMetadata({ - metadatas, - 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); + // link the newly received posts + const linkedPosts = createPostsChain({order, posts, previousPostId}); + if (linkedPosts.length) { + const postsInChannel = await this.handlePostsInChannel(linkedPosts, actionType as never, true); + if (postsInChannel.length) { + batch.push(...postsInChannel); } } - 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, - }); + if (Object.keys(postsInThread).length) { + const postsInThreads = await this.handlePostsInThread(postsInThread, actionType as never, true); + if (postsInThreads.length) { + batch.push(...postsInThreads); + } } - }; + + if (batch.length) { + await this.batchRecords(batch); + } + } /** * handleFiles: Handler responsible for the Create/Update operations occurring on the File table from the 'Server' schema @@ -241,8 +221,16 @@ const PostHandler = (superclass: any) => class extends superclass { return []; } + const processedFiles = (await this.processRecords({ + createOrUpdateRawValues: files, + tableName: FILE, + findMatchingRecordBy: isRecordFileEqualToRaw, + fieldName: 'id', + })) as ProcessRecordResults; + const postFiles = await this.prepareRecords({ - createRaws: getRawRecordPairs(files), + createRaws: processedFiles.createRaws, + updateRaws: processedFiles.updateRaws, transformer: transformFileRecord, tableName: FILE, }); @@ -267,8 +255,16 @@ const PostHandler = (superclass: any) => class extends superclass { * @returns {Promise} */ handlePostMetadata = async ({metadatas, prepareRecordsOnly}: HandlePostMetadataArgs): Promise => { + const processedMetas = (await this.processRecords({ + createOrUpdateRawValues: metadatas, + tableName: POST_METADATA, + findMatchingRecordBy: isRecordMetadataEqualToRaw, + fieldName: 'id', + })) as ProcessRecordResults; + const postMetas = await this.prepareRecords({ - createRaws: getRawRecordPairs([metadatas]), + createRaws: processedMetas.createRaws, + updateRaws: processedMetas.updateRaws, transformer: transformPostMetadataRecord, tableName: POST_METADATA, }); @@ -289,41 +285,22 @@ const PostHandler = (superclass: any) => class extends superclass { * @param {PostsInThread[]} rootPosts * @returns {Promise} */ - handlePostsInThread = async (rootPosts: PostsInThread[]): Promise => { - if (!rootPosts.length) { - return; + handlePostsInThread = async (postsMap: Record, actionType: never, prepareRecordsOnly = false): Promise => { + if (!postsMap || !Object.keys(postsMap).length) { + return []; } - - const postIds = rootPosts.map((postThread) => postThread.post_id); - const rawPostsInThreads: PostsInThread[] = []; - - // 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 PostModel[]; - - // 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: PostModel) => { - 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 PostsInThreadModel[]; - - if (postInThreadRecords?.length) { - await this.batchRecords(postInThreadRecords); + switch (actionType) { + case ActionType.POSTS.RECEIVED_IN_CHANNEL: + case ActionType.POSTS.RECEIVED_SINCE: + case ActionType.POSTS.RECEIVED_AFTER: + case ActionType.POSTS.RECEIVED_BEFORE: + return this.handleReceivedPostsInThread(postsMap, prepareRecordsOnly) as Promise; + case ActionType.POSTS.RECEIVED_NEW: { + return this.handleReceivedPostForThread(Object.values(postsMap)[0], prepareRecordsOnly) as Promise; } } + + return []; }; /** @@ -331,101 +308,30 @@ const PostHandler = (superclass: any) => class extends superclass { * @param {Post[]} posts * @returns {Promise} */ - handlePostsInChannel = async (posts: Post[]): Promise => { + handlePostsInChannel = async (posts: Post[], actionType: never, prepareRecordsOnly = false): Promise => { // 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; + const permittedActions = Object.values(ActionType.POSTS); + + if (!posts.length || !permittedActions.includes(actionType)) { + 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: Post = 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 PostsInChannelModel[]; - - 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; + switch (actionType) { + case ActionType.POSTS.RECEIVED_IN_CHANNEL: + return this.handleReceivedPostsInChannel(posts, prepareRecordsOnly) as Promise; + case ActionType.POSTS.RECEIVED_SINCE: + return this.handleReceivedPostsInChannelSince(posts, prepareRecordsOnly) as Promise; + case ActionType.POSTS.RECEIVED_AFTER: + return this.handleReceivedPostsInChannelAfter(posts, prepareRecordsOnly) as Promise; + case ActionType.POSTS.RECEIVED_BEFORE: + return this.handleReceivedPostsInChannelBefore(posts, prepareRecordsOnly) as Promise; + case ActionType.POSTS.RECEIVED_NEW: + return this.handleReceivedPostForChannel(posts[0], prepareRecordsOnly) as Promise; } - // Sort chunks (in-place) by earliest field ( oldest to newest ) - chunks.sort((a, b) => { - return a.earliest - b.earliest; - }); - - let found = false; - let targetChunk: PostsInChannelModel; - - 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 PostModel[]; - - 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(); - } - } - } else { - await createPostsInChannelRecord(); - } + return []; }; }; diff --git a/app/database/operator/server_data_operator/handlers/posts_in_channel.ts b/app/database/operator/server_data_operator/handlers/posts_in_channel.ts new file mode 100644 index 0000000000..5cdbb67d16 --- /dev/null +++ b/app/database/operator/server_data_operator/handlers/posts_in_channel.ts @@ -0,0 +1,186 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Q} from '@nozbe/watermelondb'; + +import {Database} from '@constants'; +import {retrieveRecords} from '@database/operator/utils/general'; +import {transformPostsInChannelRecord} from '@database/operator/server_data_operator/transformers/post'; +import {getPostListEdges} from '@database//operator/utils/post'; + +import type PostsInChannelModel from '@typings/database/models/servers/posts_in_channel'; + +export interface PostsInChannelHandlerMix { + handleReceivedPostsInChannel: (posts: Post[], prepareRecordsOnly?: boolean) => Promise; + handleReceivedPostsInChannelSince: (posts: Post[], prepareRecordsOnly?: boolean) => Promise; + handleReceivedPostsInChannelBefore: (posts: Post[], prepareRecordsOnly?: boolean) => Promise; + handleReceivedPostsInChannelAfter: (posts: Post[], prepareRecordsOnly?: boolean) => Promise; + handleReceivedPostForChannel: (post: Post, prepareRecordsOnly?: boolean) => Promise; +} + +const {POSTS_IN_CHANNEL} = Database.MM_TABLES.SERVER; + +const PostsInChannelHandler = (superclass: any) => class extends superclass { + _createPostsInChannelRecord = (channelId: string, earliest: number, latest: number, prepareRecordsOnly = false): Promise => { + // We should prepare instead of execute + if (prepareRecordsOnly) { + return this.prepareRecords({ + tableName: POSTS_IN_CHANNEL, + createRaws: [{record: undefined, raw: {channel_id: channelId, earliest, latest}}], + transformer: transformPostsInChannelRecord, + }); + } + return this.execute({ + createRaws: [{record: undefined, raw: {channel_id: channelId, earliest, latest}}], + tableName: POSTS_IN_CHANNEL, + transformer: transformPostsInChannelRecord, + }); + }; + + _mergePostInChannelChunks = async (newChunk: PostsInChannelModel, existingChunks: PostsInChannelModel[], prepareRecordsOnly = false) => { + const result: PostsInChannelModel[] = []; + for (const chunk of existingChunks) { + if (newChunk.earliest <= chunk.earliest && newChunk.latest >= chunk.latest) { + if (!prepareRecordsOnly) { + newChunk.prepareUpdate(); + } + result.push(newChunk); + result.push(chunk.prepareDestroyPermanently()); + break; + } + } + + if (result.length && !prepareRecordsOnly) { + await this.batchRecords(result); + } + + return result; + } + + handleReceivedPostsInChannel = async (posts: Post[], prepareRecordsOnly = false): Promise => { + if (!posts.length) { + return []; + } + + const {firstPost, lastPost} = getPostListEdges(posts); + + // Channel Id for this chain of posts + const channelId = firstPost.channel_id; + + // Find smallest 'create_at' value in chain + const earliest = firstPost.create_at; + + // Find highest 'create_at' value in chain; -1 means we are dealing with one item in the posts array + const latest = lastPost.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('id', channelId), Q.experimentalSortBy('latest', Q.desc)), + })) as PostsInChannelModel[]; + + // chunk length 0; then it's a new chunk to be added to the PostsInChannel table + if (chunks.length === 0) { + return this._createPostsInChannelRecord(channelId, earliest, latest, prepareRecordsOnly); + } + + let targetChunk: PostsInChannelModel|undefined; + + for (const chunk of chunks) { + // find if we should plug the chain before + if (firstPost.create_at >= chunk.earliest || latest <= chunk.latest) { + targetChunk = chunk; + break; + } + } + + if (targetChunk) { + // If the chunk was found, Update the chunk and return + if (prepareRecordsOnly) { + targetChunk.prepareUpdate((record) => { + record.earliest = Math.min(record.earliest, earliest); + record.latest = Math.max(record.latest, latest); + }); + return [targetChunk]; + } + + targetChunk = await this.database.action(async () => { + return targetChunk!.update((record) => { + record.earliest = Math.min(record.earliest, earliest); + record.latest = Math.max(record.latest, latest); + }); + }); + + return [targetChunk!]; + } + + // Create a new chunk and merge them if needed + const newChunk = await this._createPostsInChannelRecord(channelId, earliest, latest, prepareRecordsOnly); + const merged = await this._mergePostInChannelChunks(newChunk[0], chunks, prepareRecordsOnly); + return merged; + }; + + handleReceivedPostsInChannelSince = async (posts: Post[], prepareRecordsOnly = false): Promise => { + if (!posts.length) { + return []; + } + + const {firstPost} = getPostListEdges(posts); + let latest = 0; + + let recentChunk: PostsInChannelModel|undefined; + const chunks = (await retrieveRecords({ + database: this.database, + tableName: POSTS_IN_CHANNEL, + condition: (Q.where('id', firstPost.channel_id), Q.experimentalSortBy('latest', Q.desc)), + })) as PostsInChannelModel[]; + + if (chunks.length) { + recentChunk = chunks[0]; + + // add any new recent post while skipping the ones that were just updated + for (const post of posts) { + if (post.create_at > recentChunk.latest) { + latest = post.create_at; + } + } + } + + if (recentChunk && recentChunk.latest < latest) { + // We've got new posts that belong to this chunk + if (prepareRecordsOnly) { + recentChunk.prepareUpdate((record) => { + record.latest = Math.max(record.latest, latest); + }); + + return [recentChunk]; + } + + recentChunk = await this.database.action(async () => { + return recentChunk!.update((record) => { + record.latest = Math.max(record.latest, latest); + }); + }); + + return [recentChunk!]; + } + + return []; + }; + + handleReceivedPostsInChannelBefore = async (posts: Post[], prepareRecordsOnly = false): Promise => { + throw new Error(`handleReceivedPostsInChannelBefore Not implemented yet. posts count${posts.length} prepareRecordsOnly=${prepareRecordsOnly}`); + } + + handleReceivedPostsInChannelAfter = async (posts: Post[], prepareRecordsOnly = false): Promise => { + throw new Error(`handleReceivedPostsInChannelAfter Not implemented yet. posts count${posts.length} prepareRecordsOnly=${prepareRecordsOnly}`); + } + + handleReceivedPostForChannel = async (post: Post, prepareRecordsOnly = false): Promise => { + throw new Error(`handleReceivedPostsInChannelAfter Not implemented yet. postId ${post.id} prepareRecordsOnly=${prepareRecordsOnly}`); + } +}; + +export default PostsInChannelHandler; diff --git a/app/database/operator/server_data_operator/handlers/posts_in_thread.ts b/app/database/operator/server_data_operator/handlers/posts_in_thread.ts new file mode 100644 index 0000000000..b06d927753 --- /dev/null +++ b/app/database/operator/server_data_operator/handlers/posts_in_thread.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Q} from '@nozbe/watermelondb'; + +import {Database} from '@constants'; +import {getRawRecordPairs, retrieveRecords} from '@database/operator/utils/general'; +import {transformPostInThreadRecord} from '@database/operator/server_data_operator/transformers/post'; +import {getPostListEdges} from '@database//operator/utils/post'; + +import type PostsInThreadModel from '@typings/database/models/servers/posts_in_thread'; + +export interface PostsInThreadHandlerMix { + handleReceivedPostsInThread: (postsMap: Record, prepareRecordsOnly?: boolean) => Promise; + handleReceivedPostForThread: (post: Post, prepareRecordsOnly?: boolean) => Promise; +} + +const {POSTS_IN_THREAD} = Database.MM_TABLES.SERVER; + +const PostsInThreadHandler = (superclass: any) => class extends superclass { + handleReceivedPostsInThread = async (postsMap: Record, prepareRecordsOnly = false): Promise => { + if (!Object.keys(postsMap).length) { + return []; + } + + const update: PostsInThread[] = []; + const create: PostsInThread[] = []; + const ids = Object.keys(postsMap); + for await (const rootId of ids) { + const {firstPost, lastPost} = getPostListEdges(postsMap[rootId]); + const chunks = (await retrieveRecords({ + database: this.database, + tableName: POSTS_IN_THREAD, + condition: Q.where('id', rootId), + })) as PostsInThreadModel[]; + + if (chunks.length) { + const chunk = chunks[0]; + update.push({ + id: rootId, + earliest: Math.min(chunk.earliest, firstPost.create_at), + latest: Math.max(chunk.latest, lastPost.create_at), + }); + } else { + // create chunk + create.push({ + id: rootId, + earliest: firstPost.create_at, + latest: lastPost.create_at, + }); + } + } + + const postInThreadRecords = (await this.prepareRecords({ + createRaws: getRawRecordPairs(create), + updateRaws: getRawRecordPairs(update), + transformer: transformPostInThreadRecord, + tableName: POSTS_IN_THREAD, + })) as PostsInThreadModel[]; + + if (postInThreadRecords?.length && !prepareRecordsOnly) { + await this.batchRecords(postInThreadRecords); + } + + return postInThreadRecords; + }; + + handleReceivedPostForThread = async (post: Post, prepareRecordsOnly = false): Promise => { + throw new Error(`handleReceivedPostForThread Not implemented yet. postId ${post.id} prepareRecordsOnly=${prepareRecordsOnly}`); + } +}; + +export default PostsInThreadHandler; diff --git a/app/database/operator/server_data_operator/handlers/team.test.ts b/app/database/operator/server_data_operator/handlers/team.test.ts index a78994e419..b8e8a8f340 100644 --- a/app/database/operator/server_data_operator/handlers/team.test.ts +++ b/app/database/operator/server_data_operator/handlers/team.test.ts @@ -108,7 +108,7 @@ describe('*** Operator: Team Handlers tests ***', () => { const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords'); const myTeams = [ { - team_id: 'teamA', + id: 'teamA', roles: 'roleA, roleB, roleC', is_unread: true, mentions_count: 3, @@ -122,7 +122,7 @@ describe('*** Operator: Team Handlers tests ***', () => { expect(spyOnHandleRecords).toHaveBeenCalledTimes(1); expect(spyOnHandleRecords).toHaveBeenCalledWith({ - fieldName: 'team_id', + fieldName: 'id', createOrUpdateRawValues: myTeams, tableName: 'MyTeam', prepareRecordsOnly: false, @@ -137,7 +137,7 @@ describe('*** Operator: Team Handlers tests ***', () => { const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords'); const teamChannelHistories = [ { - team_id: 'a', + id: 'a', channel_ids: ['ca', 'cb'], }, ]; @@ -149,7 +149,7 @@ describe('*** Operator: Team Handlers tests ***', () => { expect(spyOnHandleRecords).toHaveBeenCalledTimes(1); expect(spyOnHandleRecords).toHaveBeenCalledWith({ - fieldName: 'team_id', + fieldName: 'id', createOrUpdateRawValues: teamChannelHistories, tableName: 'TeamChannelHistory', prepareRecordsOnly: false, diff --git a/app/database/operator/server_data_operator/handlers/team.ts b/app/database/operator/server_data_operator/handlers/team.ts index 02195d63d8..af11bae25d 100644 --- a/app/database/operator/server_data_operator/handlers/team.ts +++ b/app/database/operator/server_data_operator/handlers/team.ts @@ -120,10 +120,10 @@ const TeamHandler = (superclass: any) => class extends superclass { ); } - const createOrUpdateRawValues = getUniqueRawsBy({raws: teamChannelHistories, key: 'team_id'}); + const createOrUpdateRawValues = getUniqueRawsBy({raws: teamChannelHistories, key: 'id'}); return this.handleRecords({ - fieldName: 'team_id', + fieldName: 'id', findMatchingRecordBy: isRecordTeamChannelHistoryEqualToRaw, transformer: transformTeamChannelHistoryRecord, prepareRecordsOnly, @@ -204,7 +204,7 @@ const TeamHandler = (superclass: any) => class extends superclass { const createOrUpdateRawValues = getUniqueRawsBy({raws: myTeams, key: 'team_id'}); return this.handleRecords({ - fieldName: 'team_id', + fieldName: 'id', findMatchingRecordBy: isRecordMyTeamEqualToRaw, transformer: transformMyTeamRecord, prepareRecordsOnly, diff --git a/app/database/operator/server_data_operator/handlers/user.test.ts b/app/database/operator/server_data_operator/handlers/user.test.ts index a3b2c46d5b..98094c45cc 100644 --- a/app/database/operator/server_data_operator/handlers/user.test.ts +++ b/app/database/operator/server_data_operator/handlers/user.test.ts @@ -22,26 +22,29 @@ describe('*** Operator: User Handlers tests ***', () => { operator = DatabaseManager.serverDatabases['baseHandler.test.com'].operator; }); - it('=> HandleReactions: should write to both Reactions and CustomEmoji tables', async () => { + it('=> HandleReactions: should write to Reactions table', async () => { expect.assertions(2); const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords'); const spyOnBatchOperation = jest.spyOn(operator, 'batchRecords'); await operator.handleReactions({ - reactions: [ - { - create_at: 1608263728086, - emoji_name: 'p4p1', - post_id: '4r9jmr7eqt8dxq3f9woypzurry', - user_id: 'ooumoqgq3bfiijzwbn8badznwc', - }, - ], + postsReactions: [{ + post_id: '4r9jmr7eqt8dxq3f9woypzurry', + reactions: [ + { + create_at: 1608263728086, + emoji_name: 'p4p1', + post_id: '4r9jmr7eqt8dxq3f9woypzurry', + user_id: 'ooumoqgq3bfiijzwbn8badznwc', + }, + ], + }], prepareRecordsOnly: false, }); - // Called twice: Once for Reaction record and once for CustomEmoji record - expect(spyOnPrepareRecords).toHaveBeenCalledTimes(2); + // Called twice: Once for Reaction record + expect(spyOnPrepareRecords).toHaveBeenCalledTimes(1); // Only one batch operation for both tables expect(spyOnBatchOperation).toHaveBeenCalledTimes(1); diff --git a/app/database/operator/server_data_operator/handlers/user.ts b/app/database/operator/server_data_operator/handlers/user.ts index c40a3dd639..918463bf26 100644 --- a/app/database/operator/server_data_operator/handlers/user.ts +++ b/app/database/operator/server_data_operator/handlers/user.ts @@ -8,14 +8,13 @@ import { 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 {getUniqueRawsBy} from '@database/operator/utils/general'; import {sanitizeReactions} from '@database/operator/utils/reaction'; import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership'; @@ -32,7 +31,6 @@ import type UserModel from '@typings/database/models/servers/user'; const { CHANNEL_MEMBERSHIP, - CUSTOM_EMOJI, PREFERENCE, REACTION, USER, @@ -41,7 +39,7 @@ const { export interface UserHandlerMix { handleChannelMembership: ({channelMemberships, prepareRecordsOnly}: HandleChannelMembershipArgs) => Promise; handlePreferences: ({preferences, prepareRecordsOnly}: HandlePreferencesArgs) => Promise; - handleReactions: ({reactions, prepareRecordsOnly}: HandleReactionsArgs) => Promise>; + handleReactions: ({postsReactions, prepareRecordsOnly}: HandleReactionsArgs) => Promise>; handleUsers: ({users, prepareRecordsOnly}: HandleUsersArgs) => Promise; } @@ -103,54 +101,47 @@ const UserHandler = (superclass: any) => class extends superclass { /** * handleReactions: Handler responsible for the Create/Update operations occurring on the Reaction table from the 'Server' schema * @param {HandleReactionsArgs} handleReactions - * @param {Reaction[]} handleReactions.reactions + * @param {ReactionsPerPost[]} handleReactions.reactions * @param {boolean} handleReactions.prepareRecordsOnly * @throws DataOperatorException * @returns {Promise>} */ - handleReactions = async ({reactions, prepareRecordsOnly}: HandleReactionsArgs): Promise> => { - let batchRecords: Array = []; + handleReactions = async ({postsReactions, prepareRecordsOnly}: HandleReactionsArgs): Promise => { + const batchRecords: ReactionModel[] = []; - if (!reactions.length) { + if (!postsReactions.length) { throw new DataOperatorException( 'An empty "reactions" array has been passed to the handleReactions method', ); } - const rawValues = getUniqueRawsBy({raws: reactions, key: 'emoji_name'}) as Reaction[]; + for await (const postReactions of postsReactions) { + const {post_id, reactions} = postReactions; + const rawValues = getUniqueRawsBy({raws: reactions, key: 'emoji_name'}) as Reaction[]; + const { + createReactions, + deleteReactions, + } = await sanitizeReactions({ + database: this.database, + post_id, + rawReactions: rawValues, + }); - 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 ReactionModel[]; + batchRecords.push(...reactionsRecords); + } - if (createReactions.length) { - // Prepares record for model Reactions - const reactionsRecords = (await this.prepareRecords({ - createRaws: createReactions, - transformer: transformReactionRecord, - tableName: REACTION, - })) as ReactionModel[]; - batchRecords = batchRecords.concat(reactionsRecords); + if (deleteReactions?.length) { + batchRecords.push(...deleteReactions); + } } - if (createEmojis.length) { - // Prepares records for model CustomEmoji - const emojiRecords = (await this.prepareRecords({ - createRaws: getRawRecordPairs(createEmojis), - transformer: transformCustomEmojiRecord, - tableName: CUSTOM_EMOJI, - })) as CustomEmojiModel[]; - batchRecords = batchRecords.concat(emojiRecords); - } - - batchRecords = batchRecords.concat(deleteReactions); - if (prepareRecordsOnly) { return batchRecords; } diff --git a/app/database/operator/server_data_operator/index.ts b/app/database/operator/server_data_operator/index.ts index 2e8b517d90..ea4f780ed2 100644 --- a/app/database/operator/server_data_operator/index.ts +++ b/app/database/operator/server_data_operator/index.ts @@ -5,18 +5,23 @@ import ServerDataOperatorBase from '@database/operator/server_data_operator/hand 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 PostsInChannelHandler, {PostsInChannelHandlerMix} from '@database/operator/server_data_operator/handlers/posts_in_channel'; +import PostsInThreadHandler, {PostsInThreadHandlerMix} from '@database/operator/server_data_operator/handlers/posts_in_thread'; import 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 {} +interface ServerDataOperator extends ServerDataOperatorBase, PostHandlerMix, PostsInChannelHandlerMix, + PostsInThreadHandlerMix, UserHandlerMix, GroupHandlerMix, ChannelHandlerMix, TeamHandlerMix {} class ServerDataOperator extends mix(ServerDataOperatorBase).with( ChannelHandler, GroupHandler, PostHandler, + PostsInChannelHandler, + PostsInThreadHandler, TeamHandler, UserHandler, ) { diff --git a/app/database/operator/server_data_operator/transformers/channel.test.ts b/app/database/operator/server_data_operator/transformers/channel.test.ts index db9e3d9bd3..282f6b7200 100644 --- a/app/database/operator/server_data_operator/transformers/channel.test.ts +++ b/app/database/operator/server_data_operator/transformers/channel.test.ts @@ -96,6 +96,7 @@ describe('*** CHANNEL Prepare Records Test ***', () => { value: { record: undefined, raw: { + id: 'c', channel_id: 'c', guest_count: 10, header: 'channel info header', diff --git a/app/database/operator/server_data_operator/transformers/channel.ts b/app/database/operator/server_data_operator/transformers/channel.ts index 94baaaf8c1..458a2dbf26 100644 --- a/app/database/operator/server_data_operator/transformers/channel.ts +++ b/app/database/operator/server_data_operator/transformers/channel.ts @@ -65,8 +65,7 @@ export const transformMyChannelSettingsRecord = ({action, database, value}: Tran const isCreateAction = action === OperationType.CREATE; const fieldsMapper = (myChannelSetting: MyChannelSettingsModel) => { - myChannelSetting._raw.id = isCreateAction ? myChannelSetting.id : record.id; - myChannelSetting.channelId = raw.channel_id; + myChannelSetting._raw.id = isCreateAction ? (raw.channel_id || myChannelSetting.id) : record.id; myChannelSetting.notifyProps = raw.notify_props; }; @@ -92,8 +91,7 @@ export const transformChannelInfoRecord = ({action, database, value}: Transforme const isCreateAction = action === OperationType.CREATE; const fieldsMapper = (channelInfo: ChannelInfoModel) => { - channelInfo._raw.id = isCreateAction ? channelInfo.id : record.id; - channelInfo.channelId = raw.channel_id; + channelInfo._raw.id = isCreateAction ? (raw.id || channelInfo.id) : record.id; channelInfo.guestCount = raw.guest_count; channelInfo.header = raw.header; channelInfo.memberCount = raw.member_count; @@ -123,8 +121,7 @@ export const transformMyChannelRecord = ({action, database, value}: TransformerA const isCreateAction = action === OperationType.CREATE; const fieldsMapper = (myChannel: MyChannelModel) => { - myChannel._raw.id = isCreateAction ? myChannel.id : record.id; - myChannel.channelId = raw.channel_id; + myChannel._raw.id = isCreateAction ? (raw.channel_id || myChannel.id) : record.id; myChannel.roles = raw.roles; myChannel.messageCount = raw.msg_count; myChannel.mentionsCount = raw.mention_count; diff --git a/app/database/operator/server_data_operator/transformers/post.ts b/app/database/operator/server_data_operator/transformers/post.ts index 39f382aefc..e34da4964f 100644 --- a/app/database/operator/server_data_operator/transformers/post.ts +++ b/app/database/operator/server_data_operator/transformers/post.ts @@ -76,7 +76,7 @@ export const transformPostInThreadRecord = ({action, database, value}: Transform const isCreateAction = action === OperationType.CREATE; const fieldsMapper = (postsInThread: PostsInThreadModel) => { - postsInThread.postId = isCreateAction ? raw.post_id : record.id; + postsInThread._raw.id = isCreateAction ? raw.id : record.id; postsInThread.earliest = raw.earliest; postsInThread.latest = raw.latest!; }; @@ -104,7 +104,7 @@ export const transformFileRecord = ({action, database, value}: TransformerArgs): // 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: FileModel) => { - file._raw.id = isCreateAction ? (raw?.id ?? file.id) : record.id; + file._raw.id = isCreateAction ? (raw.id || file.id) : record.id; file.postId = raw.post_id; file.name = raw.name; file.extension = raw.extension; @@ -138,9 +138,8 @@ export const transformPostMetadataRecord = ({action, database, value}: Transform const isCreateAction = action === OperationType.CREATE; const fieldsMapper = (postMeta: PostMetadataModel) => { - postMeta._raw.id = isCreateAction ? postMeta.id : record.id; + postMeta._raw.id = isCreateAction ? (raw.id || postMeta.id) : record.id; postMeta.data = raw.data; - postMeta.postId = raw.post_id; }; return prepareBaseRecord({ @@ -194,8 +193,7 @@ export const transformPostsInChannelRecord = ({action, database, value}: Transfo const isCreateAction = action === OperationType.CREATE; const fieldsMapper = (postsInChannel: PostsInChannelModel) => { - postsInChannel._raw.id = isCreateAction ? postsInChannel.id : record.id; - postsInChannel.channelId = raw.channel_id; + postsInChannel._raw.id = isCreateAction ? (raw.channel_id || postsInChannel.id) : record.id; postsInChannel.earliest = raw.earliest; postsInChannel.latest = raw.latest; }; diff --git a/app/database/operator/server_data_operator/transformers/team.test.ts b/app/database/operator/server_data_operator/transformers/team.test.ts index c17246d60a..83bd02ddf9 100644 --- a/app/database/operator/server_data_operator/transformers/team.test.ts +++ b/app/database/operator/server_data_operator/transformers/team.test.ts @@ -62,7 +62,7 @@ describe('*** TEAM Prepare Records Test ***', () => { value: { record: undefined, raw: { - team_id: 'teamA', + id: 'teamA', roles: 'roleA, roleB, roleC', is_unread: true, mentions_count: 3, @@ -122,7 +122,7 @@ describe('*** TEAM Prepare Records Test ***', () => { value: { record: undefined, raw: { - team_id: 'a', + id: 'a', channel_ids: ['ca', 'cb'], }, }, diff --git a/app/database/operator/server_data_operator/transformers/team.ts b/app/database/operator/server_data_operator/transformers/team.ts index d6fc1506b1..a01c3a0172 100644 --- a/app/database/operator/server_data_operator/transformers/team.ts +++ b/app/database/operator/server_data_operator/transformers/team.ts @@ -98,8 +98,7 @@ export const transformTeamChannelHistoryRecord = ({action, database, value}: Tra const isCreateAction = action === OperationType.CREATE; const fieldsMapper = (teamChannelHistory: TeamChannelHistoryModel) => { - teamChannelHistory._raw.id = isCreateAction ? (teamChannelHistory.id) : record.id; - teamChannelHistory.teamId = raw.team_id; + teamChannelHistory._raw.id = isCreateAction ? (raw.id || teamChannelHistory.id) : record.id; teamChannelHistory.channelIds = raw.channel_ids; }; @@ -189,8 +188,7 @@ export const transformMyTeamRecord = ({action, database, value}: TransformerArgs const isCreateAction = action === OperationType.CREATE; const fieldsMapper = (myTeam: MyTeamModel) => { - myTeam._raw.id = isCreateAction ? myTeam.id : record.id; - myTeam.teamId = raw.team_id; + myTeam._raw.id = isCreateAction ? (raw.id || myTeam.id) : record.id; myTeam.roles = raw.roles; myTeam.isUnread = raw.is_unread; myTeam.mentionsCount = raw.mentions_count; diff --git a/app/database/operator/utils/post.ts b/app/database/operator/utils/post.ts index 82d37b5539..0eb216a910 100644 --- a/app/database/operator/utils/post.ts +++ b/app/database/operator/utils/post.ts @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import type {ChainPostsArgs, RecordPair, SanitizePostsArgs} from '@typings/database/database'; +import type {ChainPostsArgs, SanitizePostsArgs} from '@typings/database/database'; /** * sanitizePosts: Creates arrays of ordered and unordered posts. Unordered posts are those posts that are not @@ -32,29 +32,36 @@ export const sanitizePosts = ({posts, orders}: SanitizePostsArgs) => { * createPostsChain: Basically creates the 'chain of posts' using the 'orders' array; each post is linked to the other * by the previous_post_id field. * @param {ChainPostsArgs} chainPosts - * @param {string[]} chainPosts.orders - * @param {Post[]} chainPosts.rawPosts + * @param {string[]} chainPosts.order + * @param {Post[]} chainPosts.posts * @param {string} chainPosts.previousPostId * @returns {Post[]} */ -export const createPostsChain = ({orders, rawPosts, previousPostId = ''}: ChainPostsArgs) => { - const posts: RecordPair[] = []; +export const createPostsChain = ({order, posts, previousPostId = ''}: ChainPostsArgs) => { + return order.reduce((result, id, index) => { + const post = posts.find((p) => p.id === id); - rawPosts.forEach((post) => { - const postId = post.id; - const orderIndex = orders.findIndex((order) => { - return order === postId; - }); - - if (orderIndex === -1) { - // This case will not occur as we are using 'ordered' posts for this step. However, if this happens, that - // implies that we might be dealing with an unordered post and in which case we do not action on it. - } else if (orderIndex === 0) { - posts.push({record: undefined, raw: {...post, prev_post_id: previousPostId}}); - } else { - posts.push({record: undefined, raw: {...post, prev_post_id: orders[orderIndex - 1]}}); + if (post) { + if (index === order.length - 1) { + result.push({...post, prev_post_id: previousPostId}); + } else { + result.push({...post, prev_post_id: order[index + 1]}); + } } + + return result; + }, [] as Post[]); +}; + +export const getPostListEdges = (posts: Post[]) => { + // Sort a clone of 'posts' array by create_at + const sortedPosts = [...posts].sort((a, b) => { + return a.create_at - b.create_at; }); - return posts; + // The first element (beginning of chain) + const firstPost = sortedPosts[0]; + const lastPost = sortedPosts[sortedPosts.length - 1]; + + return {firstPost, lastPost}; }; diff --git a/app/database/operator/utils/reaction.ts b/app/database/operator/utils/reaction.ts index 7a42d4febe..72233f8691 100644 --- a/app/database/operator/utils/reaction.ts +++ b/app/database/operator/utils/reaction.ts @@ -17,7 +17,7 @@ const {REACTION} = MM_TABLES.SERVER; * @param {Database} sanitizeReactions.database * @param {string} sanitizeReactions.post_id * @param {RawReaction[]} sanitizeReactions.rawReactions - * @returns {Promise<{createReactions: RawReaction[], createEmojis: {name: string}[], deleteReactions: Reaction[]}>} + * @returns {Promise<{createReactions: RawReaction[], deleteReactions: Reaction[]}>} */ export const sanitizeReactions = async ({database, post_id, rawReactions}: SanitizeReactionsArgs) => { const reactions = (await database.collections. @@ -30,28 +30,18 @@ export const sanitizeReactions = async ({database, post_id, rawReactions}: Sanit const createReactions: RecordPair[] = []; - const emojiSet = new Set(); - for (let i = 0; i < rawReactions.length; i++) { - const rawReaction = rawReactions[i]; + const raw = rawReactions[i]; - // Do we have a similar value of rawReaction in the REACTION table? - const idxPresent = reactions.findIndex((value) => { - return ( - value.userId === rawReaction.user_id && - value.emojiName === rawReaction.emoji_name - ); - }); + // If the reaction is not present let's add it to the db + const exists = reactions.find((r) => ( + r.userId === raw.user_id && + r.emojiName === raw.emoji_name)); - if (idxPresent === -1) { - // So, we don't have a similar Reaction object. That one is new...so we'll create it - createReactions.push({record: undefined, raw: rawReaction}); - - // If that reaction is new, that implies that the emoji might also be new - emojiSet.add(rawReaction.emoji_name); + if (exists) { + similarObjects.push(exists); } else { - // we have a similar object in both reactions and rawReactions; we'll pop it out from both arrays - similarObjects.push(reactions[idxPresent]); + createReactions.push({raw}); } } @@ -60,9 +50,5 @@ export const sanitizeReactions = async ({database, post_id, rawReactions}: Sanit filter((reaction) => !similarObjects.includes(reaction)). map((outCast) => outCast.prepareDestroyPermanently()); - const createEmojis = Array.from(emojiSet).map((emoji) => { - return {name: emoji}; - }); - - return {createReactions, createEmojis, deleteReactions}; + return {createReactions, deleteReactions}; }; diff --git a/app/database/operator/utils/test.ts b/app/database/operator/utils/test.ts index b95456417c..064e85898b 100644 --- a/app/database/operator/utils/test.ts +++ b/app/database/operator/utils/test.ts @@ -22,46 +22,46 @@ describe('DataOperator: Utils tests', () => { it('=> createPostsChain: should link posts amongst each other based on order array', () => { const previousPostId = 'prev_xxyuoxmehne'; const chainedOfPosts = createPostsChain({ - orders: mockedPosts.order, - rawPosts: Object.values(mockedPosts.posts) as Post[], + order: mockedPosts.order, + posts: Object.values(mockedPosts.posts) as Post[], previousPostId, }); // eslint-disable-next-line max-nested-callbacks const post1 = chainedOfPosts.find((post) => { - const p = post.raw as Post; + const p = post; return p.id === '8swgtrrdiff89jnsiwiip3y1eoe'; - })?.raw as Post; + }); expect(post1).toBeTruthy(); - expect(post1?.prev_post_id).toBe(previousPostId); + expect(post1?.prev_post_id).toBe('8fcnk3p1jt8mmkaprgajoxz115a'); // eslint-disable-next-line max-nested-callbacks const post2 = chainedOfPosts.find((post) => { - const p = post.raw as Post; + const p = post; return p.id === '8fcnk3p1jt8mmkaprgajoxz115a'; - })?.raw as Post; + }); expect(post2).toBeTruthy(); - expect(post2!.prev_post_id).toBe('8swgtrrdiff89jnsiwiip3y1eoe'); + expect(post2!.prev_post_id).toBe('3y3w3a6gkbg73bnj3xund9o5ic'); // eslint-disable-next-line max-nested-callbacks const post3 = chainedOfPosts.find((post) => { - const p = post.raw as Post; + const p = post; return p.id === '3y3w3a6gkbg73bnj3xund9o5ic'; - })?.raw as Post; + }); expect(post3).toBeTruthy(); - expect(post3?.prev_post_id).toBe('8fcnk3p1jt8mmkaprgajoxz115a'); + expect(post3?.prev_post_id).toBe('4btbnmticjgw7ewd3qopmpiwqw'); // eslint-disable-next-line max-nested-callbacks const post4 = chainedOfPosts.find((post) => { - const p = post.raw as Post; + const p = post; return p.id === '4btbnmticjgw7ewd3qopmpiwqw'; - })?.raw as Post; + }); expect(post4).toBeTruthy(); - expect(post4!.prev_post_id).toBe('3y3w3a6gkbg73bnj3xund9o5ic'); + expect(post4!.prev_post_id).toBe(previousPostId); }); it('=> sanitizeReactions: should triage between reactions that needs creation/deletion and emojis to be created', async () => { @@ -76,14 +76,17 @@ describe('DataOperator: Utils tests', () => { // we commit one Reaction to our database const prepareRecords = await server?.operator.handleReactions({ - reactions: [ - { - user_id: 'beqkgo4wzbn98kjzjgc1p5n91o', - post_id: '8ww8kb1dbpf59fu4d5xhu5nf5w', - emoji_name: 'tada_will_be_removed', - create_at: 1601558322701, - }, - ], + postsReactions: [{ + post_id: '8ww8kb1dbpf59fu4d5xhu5nf5w', + reactions: [ + { + user_id: 'beqkgo4wzbn98kjzjgc1p5n91o', + post_id: '8ww8kb1dbpf59fu4d5xhu5nf5w', + emoji_name: 'tada_will_be_removed', + create_at: 1601558322701, + }, + ], + }], prepareRecordsOnly: true, }) as ReactionModel[]; @@ -95,7 +98,6 @@ describe('DataOperator: Utils tests', () => { const { createReactions, - createEmojis, deleteReactions, } = await sanitizeReactions({ database: server!.database, @@ -108,7 +110,5 @@ describe('DataOperator: Utils tests', () => { expect(deleteReactions[0].emojiName).toBe('tada_will_be_removed'); expect(createReactions.length).toBe(3); - - expect(createEmojis.length).toBe(3); }); }); diff --git a/app/database/schema/server/table_schemas/channel_info.ts b/app/database/schema/server/table_schemas/channel_info.ts index f3e99e8113..621944188d 100644 --- a/app/database/schema/server/table_schemas/channel_info.ts +++ b/app/database/schema/server/table_schemas/channel_info.ts @@ -10,7 +10,6 @@ const {CHANNEL_INFO} = MM_TABLES.SERVER; export default tableSchema({ name: CHANNEL_INFO, columns: [ - {name: 'channel_id', type: 'string', isIndexed: true}, {name: 'guest_count', type: 'number'}, {name: 'header', type: 'string'}, {name: 'member_count', type: 'number'}, diff --git a/app/database/schema/server/table_schemas/my_channel.ts b/app/database/schema/server/table_schemas/my_channel.ts index cc8d781c2c..2c5d300263 100644 --- a/app/database/schema/server/table_schemas/my_channel.ts +++ b/app/database/schema/server/table_schemas/my_channel.ts @@ -10,7 +10,6 @@ const {MY_CHANNEL} = MM_TABLES.SERVER; export default tableSchema({ name: MY_CHANNEL, columns: [ - {name: 'channel_id', type: 'string', isIndexed: true}, {name: 'last_post_at', type: 'number'}, {name: 'last_viewed_at', type: 'number'}, {name: 'mentions_count', type: 'number'}, diff --git a/app/database/schema/server/table_schemas/my_channel_settings.ts b/app/database/schema/server/table_schemas/my_channel_settings.ts index d28a1bc9b7..270c6ad9be 100644 --- a/app/database/schema/server/table_schemas/my_channel_settings.ts +++ b/app/database/schema/server/table_schemas/my_channel_settings.ts @@ -10,7 +10,6 @@ const {MY_CHANNEL_SETTINGS} = MM_TABLES.SERVER; export default tableSchema({ name: MY_CHANNEL_SETTINGS, columns: [ - {name: 'channel_id', type: 'string', isIndexed: true}, {name: 'notify_props', type: 'string'}, ], }); diff --git a/app/database/schema/server/table_schemas/my_team.ts b/app/database/schema/server/table_schemas/my_team.ts index b9faf8a73d..7859d5916f 100644 --- a/app/database/schema/server/table_schemas/my_team.ts +++ b/app/database/schema/server/table_schemas/my_team.ts @@ -13,6 +13,5 @@ export default tableSchema({ {name: 'is_unread', type: 'boolean'}, {name: 'mentions_count', type: 'number'}, {name: 'roles', type: 'string'}, - {name: 'team_id', type: 'string', isIndexed: true}, ], }); diff --git a/app/database/schema/server/table_schemas/post.ts b/app/database/schema/server/table_schemas/post.ts index ed655beb87..baa5afac99 100644 --- a/app/database/schema/server/table_schemas/post.ts +++ b/app/database/schema/server/table_schemas/post.ts @@ -18,7 +18,7 @@ export default tableSchema({ {name: 'is_pinned', type: 'boolean'}, {name: 'message', type: 'string'}, {name: 'original_id', type: 'string'}, - {name: 'pending_post_id', type: 'string'}, + {name: 'pending_post_id', type: 'string', isIndexed: true}, {name: 'previous_post_id', type: 'string'}, {name: 'props', type: 'string'}, {name: 'root_id', type: 'string'}, diff --git a/app/database/schema/server/table_schemas/post_metadata.ts b/app/database/schema/server/table_schemas/post_metadata.ts index 785da7e654..5ef0598a18 100644 --- a/app/database/schema/server/table_schemas/post_metadata.ts +++ b/app/database/schema/server/table_schemas/post_metadata.ts @@ -11,6 +11,5 @@ export default tableSchema({ name: POST_METADATA, columns: [ {name: 'data', type: 'string'}, - {name: 'post_id', type: 'string', isIndexed: true}, ], }); diff --git a/app/database/schema/server/table_schemas/posts_in_channel.ts b/app/database/schema/server/table_schemas/posts_in_channel.ts index 1e42d27f0d..ae1f3029ef 100644 --- a/app/database/schema/server/table_schemas/posts_in_channel.ts +++ b/app/database/schema/server/table_schemas/posts_in_channel.ts @@ -10,7 +10,6 @@ const {POSTS_IN_CHANNEL} = MM_TABLES.SERVER; export default tableSchema({ name: POSTS_IN_CHANNEL, columns: [ - {name: 'channel_id', type: 'string', isIndexed: true}, {name: 'earliest', type: 'number'}, {name: 'latest', type: 'number'}, ], diff --git a/app/database/schema/server/table_schemas/posts_in_thread.ts b/app/database/schema/server/table_schemas/posts_in_thread.ts index 4168856d90..6430e9aad1 100644 --- a/app/database/schema/server/table_schemas/posts_in_thread.ts +++ b/app/database/schema/server/table_schemas/posts_in_thread.ts @@ -12,6 +12,5 @@ export default tableSchema({ columns: [ {name: 'earliest', type: 'number'}, {name: 'latest', type: 'number'}, - {name: 'post_id', type: 'string', isIndexed: true}, ], }); diff --git a/app/database/schema/server/table_schemas/team_channel_history.ts b/app/database/schema/server/table_schemas/team_channel_history.ts index d47bb09191..e36f90cffb 100644 --- a/app/database/schema/server/table_schemas/team_channel_history.ts +++ b/app/database/schema/server/table_schemas/team_channel_history.ts @@ -11,6 +11,5 @@ export default tableSchema({ name: TEAM_CHANNEL_HISTORY, columns: [ {name: 'channel_ids', type: 'string'}, - {name: 'team_id', type: 'string', isIndexed: true}, ], }); diff --git a/app/database/schema/server/test.ts b/app/database/schema/server/test.ts index 2a0ab64816..3b65495083 100644 --- a/app/database/schema/server/test.ts +++ b/app/database/schema/server/test.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +/* eslint-disable max-lines */ + import {MM_TABLES} from '@constants/database'; import {serverSchema} from './index'; @@ -44,7 +46,6 @@ describe('*** Test schema for SERVER database ***', () => { [CHANNEL_INFO]: { name: CHANNEL_INFO, columns: { - channel_id: {name: 'channel_id', type: 'string', isIndexed: true}, guest_count: {name: 'guest_count', type: 'number'}, header: {name: 'header', type: 'string'}, member_count: {name: 'member_count', type: 'number'}, @@ -52,7 +53,6 @@ describe('*** Test schema for SERVER database ***', () => { purpose: {name: 'purpose', type: 'string'}, }, columnArray: [ - {name: 'channel_id', type: 'string', isIndexed: true}, {name: 'guest_count', type: 'number'}, {name: 'header', type: 'string'}, {name: 'member_count', type: 'number'}, @@ -110,7 +110,6 @@ describe('*** Test schema for SERVER database ***', () => { [MY_CHANNEL]: { name: MY_CHANNEL, columns: { - channel_id: {name: 'channel_id', type: 'string', isIndexed: true}, last_post_at: {name: 'last_post_at', type: 'number'}, last_viewed_at: {name: 'last_viewed_at', type: 'number'}, mentions_count: {name: 'mentions_count', type: 'number'}, @@ -118,7 +117,6 @@ describe('*** Test schema for SERVER database ***', () => { roles: {name: 'roles', type: 'string'}, }, columnArray: [ - {name: 'channel_id', type: 'string', isIndexed: true}, {name: 'last_post_at', type: 'number'}, {name: 'last_viewed_at', type: 'number'}, {name: 'mentions_count', type: 'number'}, @@ -129,23 +127,19 @@ describe('*** Test schema for SERVER database ***', () => { [MY_CHANNEL_SETTINGS]: { name: MY_CHANNEL_SETTINGS, columns: { - channel_id: {name: 'channel_id', type: 'string', isIndexed: true}, notify_props: {name: 'notify_props', type: 'string'}, }, columnArray: [ - {name: 'channel_id', type: 'string', isIndexed: true}, {name: 'notify_props', type: 'string'}, ], }, [POSTS_IN_CHANNEL]: { name: POSTS_IN_CHANNEL, columns: { - channel_id: {name: 'channel_id', type: 'string', isIndexed: true}, earliest: {name: 'earliest', type: 'number'}, latest: {name: 'latest', type: 'number'}, }, columnArray: [ - {name: 'channel_id', type: 'string', isIndexed: true}, {name: 'earliest', type: 'number'}, {name: 'latest', type: 'number'}, ], @@ -195,23 +189,19 @@ describe('*** Test schema for SERVER database ***', () => { columns: { earliest: {name: 'earliest', type: 'number'}, latest: {name: 'latest', type: 'number'}, - post_id: {name: 'post_id', type: 'string', isIndexed: true}, }, columnArray: [ {name: 'earliest', type: 'number'}, {name: 'latest', type: 'number'}, - {name: 'post_id', type: 'string', isIndexed: true}, ], }, [POST_METADATA]: { name: POST_METADATA, columns: { data: {name: 'data', type: 'string'}, - post_id: {name: 'post_id', type: 'string', isIndexed: true}, }, columnArray: [ {name: 'data', type: 'string'}, - {name: 'post_id', type: 'string', isIndexed: true}, ], }, [POST]: { @@ -225,7 +215,7 @@ describe('*** Test schema for SERVER database ***', () => { is_pinned: {name: 'is_pinned', type: 'boolean'}, message: {name: 'message', type: 'string'}, original_id: {name: 'original_id', type: 'string'}, - pending_post_id: {name: 'pending_post_id', type: 'string'}, + pending_post_id: {name: 'pending_post_id', type: 'string', isIndexed: true}, previous_post_id: {name: 'previous_post_id', type: 'string'}, props: {name: 'props', type: 'string'}, root_id: {name: 'root_id', type: 'string'}, @@ -241,7 +231,7 @@ describe('*** Test schema for SERVER database ***', () => { {name: 'is_pinned', type: 'boolean'}, {name: 'message', type: 'string'}, {name: 'original_id', type: 'string'}, - {name: 'pending_post_id', type: 'string'}, + {name: 'pending_post_id', type: 'string', isIndexed: true}, {name: 'previous_post_id', type: 'string'}, {name: 'props', type: 'string'}, {name: 'root_id', type: 'string'}, @@ -333,13 +323,11 @@ describe('*** Test schema for SERVER database ***', () => { is_unread: {name: 'is_unread', type: 'boolean'}, mentions_count: {name: 'mentions_count', type: 'number'}, roles: {name: 'roles', type: 'string'}, - team_id: {name: 'team_id', type: 'string', isIndexed: true}, }, columnArray: [ {name: 'is_unread', type: 'boolean'}, {name: 'mentions_count', type: 'number'}, {name: 'roles', type: 'string'}, - {name: 'team_id', type: 'string', isIndexed: true}, ], }, [ROLE]: { @@ -425,11 +413,9 @@ describe('*** Test schema for SERVER database ***', () => { name: TEAM_CHANNEL_HISTORY, columns: { channel_ids: {name: 'channel_ids', type: 'string'}, - team_id: {name: 'team_id', type: 'string', isIndexed: true}, }, columnArray: [ {name: 'channel_ids', type: 'string'}, - {name: 'team_id', type: 'string', isIndexed: true}, ], }, [TEAM_MEMBERSHIP]: { diff --git a/app/queries/servers/team.ts b/app/queries/servers/team.ts index 6be57d92e8..a7d981299a 100644 --- a/app/queries/servers/team.ts +++ b/app/queries/servers/team.ts @@ -9,7 +9,7 @@ export const prepareMyTeams = (operator: ServerDataOperator, teams: Team[], memb const teamMembershipRecords = operator.handleTeamMemberships({prepareRecordsOnly: true, teamMemberships: memberships}); const myTeams: MyTeam[] = unreads.map((unread) => { const matchingTeam = memberships.find((team) => team.team_id === unread.team_id); - return {team_id: unread.team_id, roles: matchingTeam?.roles ?? '', is_unread: unread.msg_count > 0, mentions_count: unread.mention_count}; + return {id: unread.team_id, roles: matchingTeam?.roles ?? '', is_unread: unread.msg_count > 0, mentions_count: unread.mention_count}; }); const myTeamRecords = operator.handleMyTeam({ prepareRecordsOnly: true, diff --git a/types/database/database.d.ts b/types/database/database.d.ts index 98445da014..8972e6a0c2 100644 --- a/types/database/database.d.ts +++ b/types/database/database.d.ts @@ -69,7 +69,7 @@ export type CreateServerDatabaseArgs = { export type HandleReactionsArgs = { prepareRecordsOnly: boolean; - reactions: Reaction[]; + postsReactions: ReactionsPerPost[]; }; export type HandleFilesArgs = { @@ -83,9 +83,10 @@ export type HandlePostMetadataArgs = { }; export type HandlePostsArgs = { - orders: string[]; + actionType: string; + order: string[]; previousPostId?: string; - values: Post[]; + posts: Post[]; }; export type SanitizeReactionsArgs = { @@ -95,9 +96,9 @@ export type SanitizeReactionsArgs = { }; export type ChainPostsArgs = { - orders: string[]; + order: string[]; previousPostId: string; - rawPosts: Post[]; + posts: Post[]; }; export type SanitizePostsArgs = { diff --git a/types/database/models/servers/channel_info.d.ts b/types/database/models/servers/channel_info.d.ts index 7c954f22eb..a4de95bedf 100644 --- a/types/database/models/servers/channel_info.d.ts +++ b/types/database/models/servers/channel_info.d.ts @@ -16,9 +16,6 @@ export default class ChannelInfoModel extends Model { /** associations : Describes every relationship to this table. */ static associations: Associations; - /** channel_id : The foreign key from CHANNEL */ - channelId: string; - /** guest_count : The number of guest in this channel */ guestCount: number; diff --git a/types/database/models/servers/my_channel.d.ts b/types/database/models/servers/my_channel.d.ts index 5a60e9bbf6..b2380030c8 100644 --- a/types/database/models/servers/my_channel.d.ts +++ b/types/database/models/servers/my_channel.d.ts @@ -14,9 +14,6 @@ export default class MyChannelModel extends Model { /** associations : Describes every relationship to this table. */ static associations: Associations; - /** channel_id : The foreign key to the related Channel record */ - channelId: string; - /** last_post_at : The timestamp for any last post on this channel */ lastPostAt: number; diff --git a/types/database/models/servers/my_channel_settings.d.ts b/types/database/models/servers/my_channel_settings.d.ts index ab3b83a812..3fb82b2d33 100644 --- a/types/database/models/servers/my_channel_settings.d.ts +++ b/types/database/models/servers/my_channel_settings.d.ts @@ -15,9 +15,6 @@ export default class MyChannelSettingsModel extends Model { /** associations : Describes every relationship to this table. */ static associations: Associations; - /** channel_id : The foreign key to the related CHANNEL record */ - channelId: string; - /** notify_props : Configurations with regards to this channel */ notifyProps: Partial; diff --git a/types/database/models/servers/my_team.d.ts b/types/database/models/servers/my_team.d.ts index 5e2ed3b85a..6864581962 100644 --- a/types/database/models/servers/my_team.d.ts +++ b/types/database/models/servers/my_team.d.ts @@ -23,9 +23,6 @@ export default class MyTeamModel extends Model { /** roles : The different permissions that this user has in the team, concatenated together with comma to form a single string. */ roles: string; - /** team_id : The foreign key of the 'parent' Team table */ - teamId: string; - /** team : The relation to the TEAM table, that this user belongs to */ team: Relation; } diff --git a/types/database/models/servers/post_metadata.d.ts b/types/database/models/servers/post_metadata.d.ts index d8a2df014a..6db02052e2 100644 --- a/types/database/models/servers/post_metadata.d.ts +++ b/types/database/models/servers/post_metadata.d.ts @@ -14,9 +14,6 @@ export default class PostMetadataModel extends Model { /** associations : Describes every relationship to this table. */ static associations: Associations; - /** post_id : The foreign key of the parent POST model */ - postId: string; - /** data : Different types of data ranging from arrays, emojis, files to images and reactions. */ data: PostMetadata; diff --git a/types/database/models/servers/posts_in_channel.d.ts b/types/database/models/servers/posts_in_channel.d.ts index 40efadea65..baddf41764 100644 --- a/types/database/models/servers/posts_in_channel.d.ts +++ b/types/database/models/servers/posts_in_channel.d.ts @@ -15,9 +15,6 @@ export default class PostsInChannelModel extends Model { /** associations : Describes every relationship to this table. */ static associations: Associations; - /** channel_id : The foreign key of the related parent channel */ - channelId: string; - /** earliest : The earliest timestamp of the post in that channel */ earliest: number; diff --git a/types/database/models/servers/posts_in_thread.d.ts b/types/database/models/servers/posts_in_thread.d.ts index e46a8dd11a..0d0653fcfa 100644 --- a/types/database/models/servers/posts_in_thread.d.ts +++ b/types/database/models/servers/posts_in_thread.d.ts @@ -21,9 +21,6 @@ export default class PostsInThreadModel extends Model { /** latest : Upper bound of a timestamp range */ latest: number; - /** post_id : The foreign key of the related Post model */ - postId: string; - /** post : The related record to the parent Post model */ post: Relation; } diff --git a/types/database/models/servers/team_channel_history.d.ts b/types/database/models/servers/team_channel_history.d.ts index fa08146ed5..788f6c90c3 100644 --- a/types/database/models/servers/team_channel_history.d.ts +++ b/types/database/models/servers/team_channel_history.d.ts @@ -15,9 +15,6 @@ export default class TeamChannelHistoryModel extends Model { /** associations : Describes every relationship to this table. */ static associations: Associations; - /** team_id : The foreign key to the related Team record */ - teamId: string; - /** channel_ids : An array containing the last 5 channels visited within this team order by recency */ channelIds: string[]; diff --git a/types/database/raw_values.d.ts b/types/database/raw_values.d.ts index 59ade07d08..a87d25dfbd 100644 --- a/types/database/raw_values.d.ts +++ b/types/database/raw_values.d.ts @@ -8,7 +8,7 @@ type AppInfo = { }; type ChannelInfo = { - channel_id: string; + id: string; guest_count: number; header: string; member_count: number; @@ -30,7 +30,7 @@ type GroupMembership = { }; type MyTeam = { - team_id: string; + id: string; roles: string; is_unread: boolean; mentions_count: number; @@ -45,12 +45,17 @@ type PostsInChannel = { type PostsInThread = { earliest: number; latest?: number; - post_id: string; + id: string; }; type Metadata = { data: PostMetadata; + id: string; +} + +type ReactionsPerPost = { post_id: string; + reactions: Reactions[]; } type IdValue = { @@ -59,7 +64,7 @@ type IdValue = { }; type TeamChannelHistory = { - team_id: string; + id: string; channel_ids: string[]; };