diff --git a/app/actions/remote/groups.ts b/app/actions/remote/groups.ts index 89b4345696..7501475c97 100644 --- a/app/actions/remote/groups.ts +++ b/app/actions/remote/groups.ts @@ -4,6 +4,7 @@ import {Client} from '@client/rest'; import DatabaseManager from '@database/manager'; import NetworkManager from '@managers/network_manager'; +import {getTeamById} from '@queries/servers/team'; import {forceLogoutIfNecessary} from './session'; @@ -57,12 +58,21 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetchOnly = false) => { try { const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); + const client: Client = NetworkManager.getClient(serverUrl); const response = await client.getAllGroupsAssociatedToTeam(teamId); - return operator.handleGroups({groups: response.groups, prepareRecordsOnly: fetchOnly}); + const [groups, groupTeams] = await Promise.all([ + operator.handleGroups({groups: response.groups, prepareRecordsOnly: true}), + operator.handleGroupTeamsForTeam({groups: response.groups, teamId, prepareRecordsOnly: true}), + ]); + + if (!fetchOnly) { + await operator.batchRecords([...groups, ...groupTeams]); + } + + return {groups, groupTeams}; } catch (error) { - forceLogoutIfNecessary(serverUrl, error as ClientErrorProps); return {error}; } }; @@ -74,8 +84,10 @@ export const fetchGroupsForMember = async (serverUrl: string, userId: string, fe const client: Client = NetworkManager.getClient(serverUrl); const response = await client.getAllGroupsAssociatedToMembership(userId); - const groups = await operator.handleGroups({groups: response, prepareRecordsOnly: true}); - const groupMemberships = await operator.handleGroupMembershipsForMember({groups: response, userId, prepareRecordsOnly: true}); + const [groups, groupMemberships] = await Promise.all([ + operator.handleGroups({groups: response, prepareRecordsOnly: true}), + operator.handleGroupMembershipsForMember({groups: response, userId, prepareRecordsOnly: true}), + ]); if (!fetchOnly) { await operator.batchRecords([...groups, ...groupMemberships]); @@ -115,3 +127,17 @@ export const fetchFilteredChannelGroups = async (serverUrl: string, searchTerm: } }; +export const fetchGroupsForTeamIfConstrained = async (serverUrl: string, teamId: string, fetchOnly = false) => { + try { + const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); + const team = await getTeamById(database, teamId); + + if (team?.isGroupConstrained) { + return fetchGroupsForTeam(serverUrl, teamId, fetchOnly); + } + + return {}; + } catch (error) { + return {error}; + } +}; diff --git a/app/actions/remote/team.ts b/app/actions/remote/team.ts index 20fea85fec..e8d918b966 100644 --- a/app/actions/remote/team.ts +++ b/app/actions/remote/team.ts @@ -16,6 +16,7 @@ import EphemeralStore from '@store/ephemeral_store'; import {isTablet} from '@utils/helpers'; import {fetchMyChannelsForTeam, switchToChannelById} from './channel'; +import {fetchGroupsForTeamIfConstrained} from './groups'; import {fetchPostsForChannel, fetchPostsForUnreadChannels} from './post'; import {fetchRolesIfNeeded} from './role'; import {forceLogoutIfNecessary} from './session'; @@ -292,4 +293,7 @@ export async function handleTeamChange(serverUrl: string, teamId: string) { await operator.batchRecords(models); } DeviceEventEmitter.emit(Events.TEAM_SWITCH, false); + + // Fetch Groups + GroupTeams + fetchGroupsForTeamIfConstrained(serverUrl, teamId); } diff --git a/app/database/operator/server_data_operator/handlers/group.ts b/app/database/operator/server_data_operator/handlers/group.ts index 3738ecd71e..9fd30204f8 100644 --- a/app/database/operator/server_data_operator/handlers/group.ts +++ b/app/database/operator/server_data_operator/handlers/group.ts @@ -2,29 +2,31 @@ // See LICENSE.txt for license information. import {MM_TABLES} from '@constants/database'; -import {transformGroupMembershipRecord, transformGroupRecord} from '@database/operator/server_data_operator/transformers/group'; +import {transformGroupMembershipRecord, transformGroupRecord, transformGroupTeamRecord} from '@database/operator/server_data_operator/transformers/group'; import {getUniqueRawsBy} from '@database/operator/utils/general'; -import {queryGroupMembershipForMember} from '@queries/servers/group'; +import {queryGroupMembershipForMember, queryGroupTeamForTeam} from '@queries/servers/group'; import {generateGroupAssociationId} from '@utils/groups'; import {logWarning} from '@utils/log'; -import type {HandleGroupArgs, HandleGroupMembershipForMemberArgs} from '@typings/database/database'; +import type {HandleGroupArgs, HandleGroupMembershipForMemberArgs, HandleGroupTeamsForTeamArgs} from '@typings/database/database'; import type GroupModel from '@typings/database/models/servers/group'; import type GroupMembershipModel from '@typings/database/models/servers/group_membership'; +import type GroupTeamModel from '@typings/database/models/servers/group_team'; -const {GROUP, GROUP_MEMBERSHIP} = MM_TABLES.SERVER; +const {GROUP, GROUP_MEMBERSHIP, GROUP_TEAM} = MM_TABLES.SERVER; export interface GroupHandlerMix { handleGroups: ({groups, prepareRecordsOnly}: HandleGroupArgs) => Promise; handleGroupMembershipsForMember: ({userId, groups, prepareRecordsOnly}: HandleGroupMembershipForMemberArgs) => Promise; + handleGroupTeamsForTeam: ({teamId, groups, prepareRecordsOnly}: HandleGroupTeamsForTeamArgs) => Promise; } const GroupHandler = (superclass: any) => class extends superclass implements GroupHandlerMix { /** * handleGroups: Handler responsible for the Create/Update operations occurring on the Group table from the 'Server' schema - * @param {HandleGroupArgs} groupsArgs - * @param {Group[]} groupsArgs.groups - * @param {boolean} groupsArgs.prepareRecordsOnly + * + * @param {HandleGroupArgs} + * @throws DataOperatorException * @returns {Promise} */ handleGroups = async ({groups, prepareRecordsOnly = true}: HandleGroupArgs): Promise => { @@ -48,10 +50,8 @@ const GroupHandler = (superclass: any) => class extends superclass implements Gr /** * handleGroupMembershipsForMember: Handler responsible for the Create/Update operations occurring on the GroupMembership table from the 'Server' schema - * @param {string} userId - * @param {HandleGroupMembershipForMemberArgs} groupMembershipsArgs - * @param {GroupMembership[]} groupMembershipsArgs.groupMemberships - * @param {boolean} groupMembershipsArgs.prepareRecordsOnly + * + * @param {HandleGroupMembershipForMemberArgs} * @throws DataOperatorException * @returns {Promise} */ @@ -73,7 +73,7 @@ const GroupHandler = (superclass: any) => class extends superclass implements Gr const groupsSet: {[key: string]: GroupMembership} = {}; for (const g of groups) { - groupsSet[`${g.id}`] = {id: generateGroupAssociationId(g.id, userId), user_id: userId, group_id: g.id}; + groupsSet[g.id] = {id: generateGroupAssociationId(g.id, userId), user_id: userId, group_id: g.id}; } for (const gm of existingGroupMemberships) { @@ -105,6 +105,64 @@ const GroupHandler = (superclass: any) => class extends superclass implements Gr return records; }; + + /** + * handleGroupTeamsForTeam: Handler responsible for the Create/Update operations occurring on the GroupTeam table from the 'Server' schema + * + * @param {HandleGroupTeamsForTeamArgs} + * @throws DataOperatorException + * @returns {Promise} + */ + handleGroupTeamsForTeam = async ({teamId, groups, prepareRecordsOnly = true}: HandleGroupTeamsForTeamArgs): Promise => { + // Get existing group teams + const existingGroupTeams = await queryGroupTeamForTeam(this.database, teamId).fetch(); + + let records: GroupTeamModel[] = []; + let rawValues: GroupTeam[] = []; + + // Nothing to add or remove + if (!groups?.length && !existingGroupTeams.length) { + return records; + } else if (!groups?.length && existingGroupTeams.length) { // No groups - remove all existing ones + records = existingGroupTeams.map((gt) => gt.prepareDestroyPermanently()); + } else if (groups?.length && !existingGroupTeams.length) { // No existing groups - add all new ones + rawValues = groups.map((g) => ({id: generateGroupAssociationId(g.id, teamId), team_id: teamId, group_id: g.id})); + } else if (groups?.length && existingGroupTeams.length) { // If both, we only want to save new ones and delete one's no longer in groups + const groupsSet: {[key: string]: GroupTeam} = {}; + + for (const g of groups) { + groupsSet[g.id] = {id: generateGroupAssociationId(g.id, teamId), team_id: teamId, group_id: g.id}; + } + + for (const gt of existingGroupTeams) { + // Check if existingGroups overlaps with groups + if (groupsSet[gt.groupId]) { + // If there is an existing group already, we don't need to add it + delete groupsSet[gt.groupId]; + } else { + // No group? Remove existing one + records.push(gt.prepareDestroyPermanently()); + } + } + + rawValues.push(...Object.values(groupsSet)); + } + + records.push(...(await this.handleRecords({ + fieldName: 'id', + transformer: transformGroupTeamRecord, + rawValues, + tableName: GROUP_TEAM, + prepareRecordsOnly: true, + }))); + + // Batch update if there are records + if (records.length && !prepareRecordsOnly) { + await this.batchRecords(records); + } + + return records; + }; }; export default GroupHandler; diff --git a/app/database/operator/server_data_operator/transformers/group.ts b/app/database/operator/server_data_operator/transformers/group.ts index 60a137c1b3..6888a1fa65 100644 --- a/app/database/operator/server_data_operator/transformers/group.ts +++ b/app/database/operator/server_data_operator/transformers/group.ts @@ -10,10 +10,12 @@ import {generateGroupAssociationId} from '@utils/groups'; import type {TransformerArgs} from '@typings/database/database'; import type GroupModel from '@typings/database/models/servers/group'; import type GroupMembershipModel from '@typings/database/models/servers/group_membership'; +import type GroupTeamModel from '@typings/database/models/servers/group_team'; const { GROUP, GROUP_MEMBERSHIP, + GROUP_TEAM, } = MM_TABLES.SERVER; /** @@ -72,3 +74,29 @@ export const transformGroupMembershipRecord = ({action, database, value}: Transf fieldsMapper, }) as Promise; }; + +/** + * transformGroupTeamRecord: Prepares a record of the SERVER database 'GroupTeam' table for update or create actions. + * @param {TransformerArgs} operator + * @param {Database} operator.database + * @param {RecordPair} operator.value + * @returns {Promise} + */ +export const transformGroupTeamRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw as GroupTeam; + + // id of group comes from server response + const fieldsMapper = (model: GroupTeamModel) => { + model._raw.id = raw.id || generateGroupAssociationId(raw.group_id, raw.team_id); + model.groupId = raw.group_id; + model.teamId = raw.team_id; + }; + + return prepareBaseRecord({ + action, + database, + tableName: GROUP_TEAM, + value, + fieldsMapper, + }) as Promise; +}; diff --git a/app/queries/servers/group.ts b/app/queries/servers/group.ts index 82be34cc0e..5867041065 100644 --- a/app/queries/servers/group.ts +++ b/app/queries/servers/group.ts @@ -7,6 +7,7 @@ import {MM_TABLES} from '@constants/database'; import type GroupModel from '@typings/database/models/servers/group'; import type GroupMembershipModel from '@typings/database/models/servers/group_membership'; +import type GroupTeamModel from '@typings/database/models/servers/group_team'; const {SERVER: {GROUP, GROUP_CHANNEL, GROUP_MEMBERSHIP, GROUP_TEAM}} = MM_TABLES; @@ -41,3 +42,9 @@ export const queryGroupMembershipForMember = (database: Database, userId: string Q.where('user_id', userId), ); }; + +export const queryGroupTeamForTeam = (database: Database, teamId: string) => { + return database.collections.get(GROUP_TEAM).query( + Q.where('team_id', teamId), + ); +}; diff --git a/types/api/groups.d.ts b/types/api/groups.d.ts index 152c7ea7ba..9b88f2cd27 100644 --- a/types/api/groups.d.ts +++ b/types/api/groups.d.ts @@ -16,14 +16,9 @@ type Group = { }; type GroupTeam = { + id?: string; team_id: string; - team_display_name: string; - team_type: string; group_id: string; - auto_add: boolean; - create_at: number; - delete_at: number; - update_at: number; } type GroupChannel = { diff --git a/types/database/database.d.ts b/types/database/database.d.ts index bdfbaecac8..000cb6b2cc 100644 --- a/types/database/database.d.ts +++ b/types/database/database.d.ts @@ -227,6 +227,11 @@ export type HandleGroupMembershipForMemberArgs = PrepareOnly & { groups?: Group[]; } +export type HandleGroupTeamsForTeamArgs = PrepareOnly & { + teamId: string; + groups?: Group[]; +} + export type HandleCategoryChannelArgs = PrepareOnly & { categoryChannels?: CategoryChannel[]; };