[Gekidou DB]: Adds threads in team database table and handlers (#6090)

* Adds threads in team database table and handlers

DM/GM channels have no team ID. This makes it troublesome to paginate
threads in a team. The issue is that whenever a DM/GM thread is fetched
from pagination it will be added in all teams in that server,
potentially creating gaps in between threads for those teams.

This PR inserts a new table in the DB ThreadsInTeam which will hold
references of threads loaded in which server.
Thread lists then would have to rely on that table to show threads.


Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
Co-authored-by: Avinash Lingaloo <avinashlng1080@gmail.com>
This commit is contained in:
Kyriakos Z 2022-03-28 10:11:13 +03:00 committed by GitHub
parent 86ae1fc9cc
commit f2484297a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 331 additions and 12 deletions

View file

@ -35,6 +35,7 @@ export const MM_TABLES = {
TEAM_SEARCH_HISTORY: 'TeamSearchHistory',
TERMS_OF_SERVICE: 'TermsOfService',
THREAD: 'Thread',
THREADS_IN_TEAM: 'ThreadsInTeam',
THREAD_PARTICIPANT: 'ThreadParticipant',
USER: 'User',
},

View file

@ -15,7 +15,7 @@ import {CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, Cha
MyChannelModel, MyChannelSettingsModel, MyTeamModel,
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
SlashCommandModel, SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
TermsOfServiceModel, ThreadModel, ThreadParticipantModel, UserModel,
TermsOfServiceModel, ThreadModel, ThreadParticipantModel, ThreadInTeamModel, UserModel,
} from '@database/models/server';
import AppDataOperator from '@database/operator/app_data_operator';
import ServerDataOperator from '@database/operator/server_data_operator';
@ -51,7 +51,7 @@ class DatabaseManager {
MyChannelModel, MyChannelSettingsModel, MyTeamModel,
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
SlashCommandModel, SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
TermsOfServiceModel, ThreadModel, ThreadParticipantModel, UserModel,
TermsOfServiceModel, ThreadModel, ThreadParticipantModel, ThreadInTeamModel, UserModel,
];
this.databaseDirectory = '';
}

View file

@ -16,7 +16,7 @@ import {CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, Cha
MyChannelModel, MyChannelSettingsModel, MyTeamModel,
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
SlashCommandModel, SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
TermsOfServiceModel, ThreadModel, ThreadParticipantModel, UserModel,
TermsOfServiceModel, ThreadModel, ThreadParticipantModel, ThreadInTeamModel, UserModel,
} from '@database/models/server';
import AppDataOperator from '@database/operator/app_data_operator';
import ServerDataOperator from '@database/operator/server_data_operator';
@ -46,7 +46,7 @@ class DatabaseManager {
MyChannelModel, MyChannelSettingsModel, MyTeamModel,
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
SlashCommandModel, SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
TermsOfServiceModel, ThreadModel, ThreadParticipantModel, UserModel,
TermsOfServiceModel, ThreadModel, ThreadParticipantModel, ThreadInTeamModel, UserModel,
];
this.databaseDirectory = Platform.OS === 'ios' ? getIOSAppGroupDetails().appGroupDatabase : `${FileSystem.documentDirectory}databases/`;

View file

@ -26,5 +26,6 @@ export {default as TeamModel} from './team';
export {default as TeamSearchHistoryModel} from './team_search_history';
export {default as TermsOfServiceModel} from './terms_of_service';
export {default as ThreadModel} from './thread';
export {default as ThreadInTeamModel} from './thread_in_team';
export {default as ThreadParticipantModel} from './thread_participant';
export {default as UserModel} from './user';

View file

@ -1,8 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Relation} from '@nozbe/watermelondb';
import {children, field, immutableRelation} from '@nozbe/watermelondb/decorators';
import {Q, Relation} from '@nozbe/watermelondb';
import {children, field, immutableRelation, lazy} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
@ -14,6 +14,7 @@ import type SlashCommandModel from '@typings/database/models/servers/slash_comma
import type TeamChannelHistoryModel from '@typings/database/models/servers/team_channel_history';
import type TeamMembershipModel from '@typings/database/models/servers/team_membership';
import type TeamSearchHistoryModel from '@typings/database/models/servers/team_search_history';
import type ThreadModel from '@typings/database/models/servers/thread';
const {
CATEGORY,
@ -24,6 +25,8 @@ const {
TEAM_CHANNEL_HISTORY,
TEAM_MEMBERSHIP,
TEAM_SEARCH_HISTORY,
THREADS_IN_TEAM,
THREAD,
} = MM_TABLES.SERVER;
/**
@ -53,6 +56,9 @@ export default class TeamModel extends Model {
/** A TEAM has a 1:N relationship with TEAM_SEARCH_HISTORY. A TEAM can possess multiple search histories*/
[TEAM_SEARCH_HISTORY]: {type: 'has_many', foreignKey: 'team_id'},
/** A TEAM has a 1:N relationship with THREADS_IN_TEAM. A TEAM can possess multiple threads */
[THREADS_IN_TEAM]: {type: 'has_many', foreignKey: 'team_id'},
};
/** is_allow_open_invite : Boolean flag indicating if this team is open to the public */
@ -102,4 +108,21 @@ export default class TeamModel extends Model {
/** teamSearchHistories : All the searches performed on this team */
@children(TEAM_SEARCH_HISTORY) teamSearchHistories!: TeamSearchHistoryModel[];
/** threads : All threads belonging to a team */
@lazy threads = this.collections.get<ThreadModel>(THREAD).query(
Q.on(THREADS_IN_TEAM, 'team_id', this.id),
);
/** threads : Threads list belonging to a team */
@lazy threadsList = this.threads.extend(
Q.where('loadedInGlobalThreads', true),
Q.sortBy('last_reply_at', Q.desc),
);
/** unreadThreadsList : Unread threads list belonging to a team */
@lazy unreadThreadsList = this.threads.extend(
Q.where('unread_replies', Q.gt(0)),
Q.sortBy('last_reply_at', Q.desc),
);
}

View file

@ -8,9 +8,10 @@ import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import type PostModel from '@typings/database/models/servers/post';
import type ThreadInTeamModel from '@typings/database/models/servers/thread_in_team';
import type ThreadParticipantModel from '@typings/database/models/servers/thread_participant';
const {POST, THREAD, THREAD_PARTICIPANT} = MM_TABLES.SERVER;
const {POST, THREAD, THREAD_PARTICIPANT, THREADS_IN_TEAM} = MM_TABLES.SERVER;
/**
* The Thread model contains thread information of a post.
@ -27,6 +28,9 @@ export default class ThreadModel extends Model {
/** A THREAD can have multiple THREAD_PARTICIPANT. (relationship is 1:N)*/
[THREAD_PARTICIPANT]: {type: 'has_many', foreignKey: 'thread_id'},
/** A THREAD can have multiple THREADS_IN_TEAM. (relationship is 1:N)*/
[THREADS_IN_TEAM]: {type: 'has_many', foreignKey: 'team_id'},
};
/** last_reply_at : The timestamp of when user last replied to the thread. */
@ -53,11 +57,15 @@ export default class ThreadModel extends Model {
/** participants : All the participants associated with this Thread */
@children(THREAD_PARTICIPANT) participants!: Query<ThreadParticipantModel>;
/** threadsInTeam : All the threadsInTeam associated with this Thread */
@children(THREADS_IN_TEAM) threadsInTeam!: Query<ThreadInTeamModel>;
/** post : The root post of this thread */
@immutableRelation(POST, 'id') post!: Relation<PostModel>;
async destroyPermanently() {
await this.participants.destroyAllPermanently();
await this.threadsInTeam.destroyAllPermanently();
super.destroyPermanently();
}
}

View file

@ -0,0 +1,42 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Relation} from '@nozbe/watermelondb';
import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import type TeamModel from '@typings/database/models/servers/team';
import type ThreadModel from '@typings/database/models/servers/thread';
const {TEAM, THREAD, THREADS_IN_TEAM} = MM_TABLES.SERVER;
/**
* ThreadInTeam model helps us to combine adjacent threads together without leaving
* gaps in between for an efficient user reading experience for threads.
*/
export default class ThreadInTeamModel extends Model {
/** table (name) : ThreadsInTeam */
static table = THREADS_IN_TEAM;
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A TEAM can have many THREADS_IN_TEAM. (relationship is N:1)*/
[TEAM]: {type: 'belongs_to', key: 'team_id'},
/** A THREAD can have many THREADS_IN_TEAM. (relationship is N:1)*/
[THREAD]: {type: 'belongs_to', key: 'team_id'},
};
/** thread_id: Associated thread identifier */
@field('thread_id') threadId!: string;
/** team_id: Associated team identifier */
@field('team_id') teamId!: string;
@immutableRelation(THREAD, 'thread_id') thread!: Relation<ThreadModel>;
@immutableRelation(TEAM, 'team_id') team!: Relation<TeamModel>;
}

View file

@ -3,7 +3,7 @@
import DatabaseManager from '@database/manager';
import {isRecordThreadEqualToRaw} from '@database/operator/server_data_operator/comparators';
import {transformThreadRecord, transformThreadParticipantRecord} from '@database/operator/server_data_operator/transformers/thread';
import {transformThreadRecord, transformThreadParticipantRecord, transformThreadInTeamRecord} from '@database/operator/server_data_operator/transformers/thread';
import ServerDataOperator from '..';
@ -27,10 +27,13 @@ describe('*** Operator: Thread Handlers tests ***', () => {
operator = DatabaseManager.serverDatabases['baseHandler.test.com'].operator;
});
it('=> HandleThreads: should write to the the Thread & ThreadParticipant table', async () => {
it('=> HandleThreads: should write to the the Thread & ThreadParticipant & ThreadsInTeam tables', async () => {
expect.assertions(4);
const spyOnBatchOperation = jest.spyOn(operator, 'batchRecords');
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const spyOnHandleThreadParticipants = jest.spyOn(operator, 'handleThreadParticipants');
const spyOnHandleThreadInTeam = jest.spyOn(operator, 'handleThreadInTeam');
const threads = [
{
@ -47,7 +50,8 @@ describe('*** Operator: Thread Handlers tests ***', () => {
},
] as Thread[];
await operator.handleThreads({threads, prepareRecordsOnly: false});
const threadsMap = {team_id_1: threads};
await operator.handleThreads({threads, prepareRecordsOnly: false, teamId: 'team_id_1'});
expect(spyOnHandleRecords).toHaveBeenCalledWith({
findMatchingRecordBy: isRecordThreadEqualToRaw,
@ -70,11 +74,18 @@ describe('*** Operator: Thread Handlers tests ***', () => {
prepareRecordsOnly: true,
});
expect(spyOnHandleThreadInTeam).toHaveBeenCalledWith({
threadsMap,
prepareRecordsOnly: true,
});
// Only one batch operation for both tables
expect(spyOnBatchOperation).toHaveBeenCalledTimes(1);
});
it('=> HandleThreadParticipants: should write to the the ThreadParticipant table', async () => {
expect.assertions(1);
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
const threadsParticipants = [
@ -97,4 +108,70 @@ describe('*** Operator: Thread Handlers tests ***', () => {
tableName: 'ThreadParticipant',
});
});
it('=> HandleThreadInTeam: should write to the the ThreadsInTeam table', async () => {
expect.assertions(1);
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
const team1Threads = [
{
id: 'thread-1',
reply_count: 2,
last_reply_at: 123,
last_viewed_at: 123,
participants: [{
id: 'user-1',
}],
is_following: true,
unread_replies: 0,
unread_mentions: 0,
},
{
id: 'thread-2',
reply_count: 2,
last_reply_at: 123,
last_viewed_at: 123,
participants: [{
id: 'user-1',
}],
is_following: true,
unread_replies: 0,
unread_mentions: 0,
},
] as Thread[];
const team2Threads = [
{
id: 'thread-2',
reply_count: 2,
last_reply_at: 123,
last_viewed_at: 123,
participants: [{
id: 'user-1',
}],
is_following: true,
unread_replies: 0,
unread_mentions: 0,
},
] as Thread[];
const threadsMap = {
team_id_1: team1Threads,
team_id_2: team2Threads,
};
await operator.handleThreadInTeam({threadsMap, prepareRecordsOnly: false});
expect(spyOnPrepareRecords).toHaveBeenCalledWith({
createRaws: [{
raw: {team_id: 'team_id_1', thread_id: 'thread-2'},
}, {
raw: {team_id: 'team_id_2', thread_id: 'thread-2'},
}],
transformer: transformThreadInTeamRecord,
updateRaws: [],
tableName: 'ThreadsInTeam',
});
});
});

View file

@ -15,6 +15,7 @@ import {sanitizeThreadParticipants} from '@database/operator/utils/thread';
import type {HandleThreadsArgs, HandleThreadParticipantsArgs} from '@typings/database/database';
import type ThreadModel from '@typings/database/models/servers/thread';
import type ThreadInTeamModel from '@typings/database/models/servers/thread_in_team';
import type ThreadParticipantModel from '@typings/database/models/servers/thread_participant';
const {
@ -35,7 +36,7 @@ const ThreadHandler = (superclass: any) => class extends superclass {
* @param {boolean | undefined} handleThreads.prepareRecordsOnly
* @returns {Promise<void>}
*/
handleThreads = async ({threads, prepareRecordsOnly = false}: HandleThreadsArgs): Promise<Model[]> => {
handleThreads = async ({threads, teamId, prepareRecordsOnly = false}: HandleThreadsArgs): Promise<Model[]> => {
if (!threads.length) {
throw new DataOperatorException(
'An empty "threads" array has been passed to the handleThreads method',
@ -78,6 +79,12 @@ const ThreadHandler = (superclass: any) => class extends superclass {
const threadParticipants = (await this.handleThreadParticipants({threadsParticipants, prepareRecordsOnly: true})) as ThreadParticipantModel[];
batch.push(...threadParticipants);
const threadsInTeam = await this.handleThreadInTeam({
threadsMap: {[teamId]: threads},
prepareRecordsOnly: true,
}) as ThreadInTeamModel[];
batch.push(...threadsInTeam);
if (batch.length && !prepareRecordsOnly) {
await this.batchRecords(batch);
}

View file

@ -0,0 +1,62 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Q, Database} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
import {transformThreadInTeamRecord} from '@database/operator/server_data_operator/transformers/thread';
import {getRawRecordPairs} from '@database/operator/utils/general';
import type {HandleThreadInTeamArgs} from '@typings/database/database';
import type ThreadInTeamModel from '@typings/database/models/servers/thread_in_team';
export interface ThreadInTeamHandlerMix {
handleThreadInTeam: ({threadsMap, prepareRecordsOnly}: HandleThreadInTeamArgs) => Promise<ThreadInTeamModel[]>;
}
const {THREADS_IN_TEAM} = MM_TABLES.SERVER;
const ThreadInTeamHandler = (superclass: any) => class extends superclass {
handleThreadInTeam = async ({threadsMap, prepareRecordsOnly = false}: HandleThreadInTeamArgs): Promise<ThreadInTeamModel[]> => {
if (!Object.keys(threadsMap).length) {
return [];
}
const create: ThreadInTeam[] = [];
const teamIds = Object.keys(threadsMap);
for await (const teamId of teamIds) {
const chunks = await (this.database as Database).get<ThreadInTeamModel>(THREADS_IN_TEAM).query(
Q.where('team_id', teamId),
).fetch();
for (const thread of threadsMap[teamId]) {
const exists = chunks.some((threadInTeam) => {
return threadInTeam.threadId === thread.id;
});
if (!exists) {
// create chunk
create.push({
thread_id: thread.id,
team_id: teamId,
});
}
}
}
const threadsInTeam = (await this.prepareRecords({
createRaws: getRawRecordPairs(create),
updateRaws: [],
transformer: transformThreadInTeamRecord,
tableName: THREADS_IN_TEAM,
})) as ThreadInTeamModel[];
if (threadsInTeam?.length && !prepareRecordsOnly) {
await this.batchRecords(threadsInTeam);
}
return threadsInTeam;
};
};
export default ThreadInTeamHandler;

View file

@ -10,13 +10,14 @@ import PostsInThreadHandler, {PostsInThreadHandlerMix} from '@database/operator/
import ReactionHander, {ReactionHandlerMix} from '@database/operator/server_data_operator/handlers/reaction';
import TeamHandler, {TeamHandlerMix} from '@database/operator/server_data_operator/handlers/team';
import ThreadHandler, {ThreadHandlerMix} from '@database/operator/server_data_operator/handlers/thread';
import ThreadInTeamHandler, {ThreadInTeamHandlerMix} from '@database/operator/server_data_operator/handlers/thread_in_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, PostsInChannelHandlerMix,
PostsInThreadHandlerMix, ReactionHandlerMix, UserHandlerMix, ChannelHandlerMix, CategoryHandlerMix, TeamHandlerMix, ThreadHandlerMix {}
PostsInThreadHandlerMix, ReactionHandlerMix, UserHandlerMix, ChannelHandlerMix, CategoryHandlerMix, TeamHandlerMix, ThreadHandlerMix, ThreadInTeamHandlerMix {}
class ServerDataOperator extends mix(ServerDataOperatorBase).with(
CategoryHandler,
@ -27,6 +28,7 @@ class ServerDataOperator extends mix(ServerDataOperatorBase).with(
ReactionHander,
TeamHandler,
ThreadHandler,
ThreadInTeamHandler,
UserHandler,
) {
// eslint-disable-next-line no-useless-constructor

View file

@ -7,11 +7,13 @@ import {OperationType} from '@typings/database/enums';
import type {TransformerArgs} from '@typings/database/database';
import type ThreadModel from '@typings/database/models/servers/thread';
import type ThreadInTeamModel from '@typings/database/models/servers/thread_in_team';
import type ThreadParticipantModel from '@typings/database/models/servers/thread_participant';
const {
THREAD,
THREAD_PARTICIPANT,
THREADS_IN_TEAM,
} = MM_TABLES.SERVER;
/**
@ -71,3 +73,20 @@ export const transformThreadParticipantRecord = ({action, database, value}: Tran
fieldsMapper,
}) as Promise<ThreadParticipantModel>;
};
export const transformThreadInTeamRecord = ({action, database, value}: TransformerArgs): Promise<ThreadInTeamModel> => {
const raw = value.raw as ThreadInTeam;
const fieldsMapper = (threadInTeam: ThreadInTeamModel) => {
threadInTeam.threadId = raw.thread_id;
threadInTeam.teamId = raw.team_id;
};
return prepareBaseRecord({
action,
database,
tableName: THREADS_IN_TEAM,
value,
fieldsMapper,
}) as Promise<ThreadInTeamModel>;
};

View file

@ -29,6 +29,7 @@ import {
TeamSearchHistorySchema,
TermsOfServiceSchema,
ThreadSchema,
ThreadInTeamSchema,
ThreadParticipantSchema,
UserSchema,
} from './table_schemas';
@ -61,6 +62,7 @@ export const serverSchema: AppSchema = appSchema({
TeamSearchHistorySchema,
TermsOfServiceSchema,
ThreadSchema,
ThreadInTeamSchema,
ThreadParticipantSchema,
UserSchema,
],

View file

@ -27,4 +27,5 @@ export {default as TeamSearchHistorySchema} from './team_search_history';
export {default as TermsOfServiceSchema} from './terms_of_service';
export {default as ThreadSchema} from './thread';
export {default as ThreadParticipantSchema} from './thread_participant';
export {default as ThreadInTeamSchema} from './thread_in_team';
export {default as UserSchema} from './user';

View file

@ -0,0 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {tableSchema} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
const {THREADS_IN_TEAM} = MM_TABLES.SERVER;
export default tableSchema({
name: THREADS_IN_TEAM,
columns: [
{name: 'team_id', type: 'string', isIndexed: true},
{name: 'thread_id', type: 'string', isIndexed: true},
],
});

View file

@ -34,6 +34,7 @@ const {
TERMS_OF_SERVICE,
THREAD,
THREAD_PARTICIPANT,
THREADS_IN_TEAM,
USER,
} = MM_TABLES.SERVER;
@ -495,6 +496,18 @@ describe('*** Test schema for SERVER database ***', () => {
{name: 'user_id', type: 'string', isIndexed: true},
],
},
[THREADS_IN_TEAM]: {
name: THREADS_IN_TEAM,
unsafeSql: undefined,
columns: {
team_id: {name: 'team_id', type: 'string', isIndexed: true},
thread_id: {name: 'thread_id', type: 'string', isIndexed: true},
},
columnArray: [
{name: 'team_id', type: 'string', isIndexed: true},
{name: 'thread_id', type: 'string', isIndexed: true},
],
},
[USER]: {
name: USER,
unsafeSql: undefined,

View file

@ -91,6 +91,7 @@ export type HandlePostsArgs = {
export type HandleThreadsArgs = {
threads: Thread[];
prepareRecordsOnly?: boolean;
teamId: string;
};
export type HandleThreadParticipantsArgs = {
@ -98,6 +99,11 @@ export type HandleThreadParticipantsArgs = {
threadsParticipants: ParticipantsPerThread[];
};
export type HandleThreadInTeamArgs = {
threadsMap: Record<string, Thread[]>;
prepareRecordsOnly?: boolean;
};
export type SanitizeReactionsArgs = {
database: Database;
post_id: string;

View file

@ -0,0 +1,33 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Relation} from '@nozbe/watermelondb';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import type TeamModel from './team';
import type ThreadModel from './thread';
/**
* ThreadInTeam model helps us to determine for which team we have loaded which threads
* this was done mostly because of threads belonging in DM/GM channels,
* so they belong in multiple teams.
*/
export default class ThreadInTeamModel extends Model {
/** table (name) : ThreadsInTeam */
static table: string;
/** associations : Describes every relationship to this table. */
static associations: Associations;
/** thread_id: Associated thread identifier */
threadId: string;
/** teamId: Associated thread identifier */
teamId: string;
/** thread : The related record to the parent Thread model */
thread: Relation<ThreadModel>;
/** team : The related record to the parent Team model */
team: Relation<TeamModel>;
}

View file

@ -82,6 +82,11 @@ type TermsOfService = {
text: string;
};
type ThreadInTeam = {
thread_id: string;
team_id: string;
};
type RawValue =
| AppInfo
| Category
@ -109,6 +114,7 @@ type RawValue =
| TeamSearchHistory
| TermsOfService
| Thread
| ThreadInTeam
| ThreadParticipant
| UserProfile
| Pick<ChannelMembership, 'channel_id' | 'user_id'>