[Gekidou] fix database schema and models (#5553)

* fix database schema and models

* fix types
This commit is contained in:
Elias Nahum
2021-07-20 15:24:42 -04:00
committed by GitHub
parent bd0f22fcd1
commit 324dbbd054
60 changed files with 781 additions and 554 deletions

View File

@@ -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;
};

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<CustomEmojiModel[]>;
}
@@ -104,7 +106,7 @@ export default class ServerDataOperatorBase extends BaseDataOperator {
* @param {(TransformerArgs) => Promise<Model>} execute.recordOperator
* @returns {Promise<void>}
*/
execute = async ({createRaws, transformer, tableName, updateRaws}: OperationArgs): Promise<void> => {
execute = async ({createRaws, transformer, tableName, updateRaws}: OperationArgs): Promise<Model[]> => {
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;
};
}

View File

@@ -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<string, Post[]> = {};
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);
});
});

View File

@@ -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<DraftModel[]>;
handleFiles: ({files, prepareRecordsOnly}: HandleFilesArgs) => Promise<FileModel[]>;
handlePostMetadata: ({metadatas, prepareRecordsOnly}: HandlePostMetadataArgs) => Promise<PostMetadataModel[]>;
handlePosts: ({orders, values, previousPostId}: HandlePostsArgs) => Promise<void>;
handlePosts: ({actionType, order, posts, previousPostId}: HandlePostsArgs) => Promise<void>;
handlePostsInChannel: (posts: Post[]) => Promise<void>;
handlePostsInThread: (rootPosts: PostsInThread[]) => Promise<void>;
}
@@ -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<void>}
*/
handlePosts = async ({orders, values, previousPostId}: HandlePostsArgs): Promise<void> => {
handlePosts = async ({actionType, order, posts, previousPostId = ''}: HandlePostsArgs): Promise<void> => {
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<string, Post[]> = {};
// 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<PostMetadataModel[]>}
*/
handlePostMetadata = async ({metadatas, prepareRecordsOnly}: HandlePostMetadataArgs): Promise<PostMetadataModel[]> => {
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<void>}
*/
handlePostsInThread = async (rootPosts: PostsInThread[]): Promise<void> => {
if (!rootPosts.length) {
return;
handlePostsInThread = async (postsMap: Record<string, Post[]>, actionType: never, prepareRecordsOnly = false): Promise<PostsInThreadModel[]> => {
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<PostsInThreadModel[]>;
case ActionType.POSTS.RECEIVED_NEW: {
return this.handleReceivedPostForThread(Object.values(postsMap)[0], prepareRecordsOnly) as Promise<PostsInThreadModel[]>;
}
}
return [];
};
/**
@@ -331,101 +308,30 @@ const PostHandler = (superclass: any) => class extends superclass {
* @param {Post[]} posts
* @returns {Promise<void>}
*/
handlePostsInChannel = async (posts: Post[]): Promise<void> => {
handlePostsInChannel = async (posts: Post[], actionType: never, prepareRecordsOnly = false): Promise<PostsInChannelModel[]> => {
// 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<PostsInChannelModel[]>;
case ActionType.POSTS.RECEIVED_SINCE:
return this.handleReceivedPostsInChannelSince(posts, prepareRecordsOnly) as Promise<PostsInChannelModel[]>;
case ActionType.POSTS.RECEIVED_AFTER:
return this.handleReceivedPostsInChannelAfter(posts, prepareRecordsOnly) as Promise<PostsInChannelModel[]>;
case ActionType.POSTS.RECEIVED_BEFORE:
return this.handleReceivedPostsInChannelBefore(posts, prepareRecordsOnly) as Promise<PostsInChannelModel[]>;
case ActionType.POSTS.RECEIVED_NEW:
return this.handleReceivedPostForChannel(posts[0], prepareRecordsOnly) as Promise<PostsInChannelModel[]>;
}
// 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 [];
};
};

View File

@@ -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<PostsInChannelModel[]>;
handleReceivedPostsInChannelSince: (posts: Post[], prepareRecordsOnly?: boolean) => Promise<PostsInChannelModel[]>;
handleReceivedPostsInChannelBefore: (posts: Post[], prepareRecordsOnly?: boolean) => Promise<PostsInChannelModel[]>;
handleReceivedPostsInChannelAfter: (posts: Post[], prepareRecordsOnly?: boolean) => Promise<PostsInChannelModel[]>;
handleReceivedPostForChannel: (post: Post, prepareRecordsOnly?: boolean) => Promise<PostsInChannelModel[]>;
}
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<PostsInChannelModel[]> => {
// 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<PostsInChannelModel[]> => {
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<PostsInChannelModel[]> => {
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<PostsInChannelModel[]> => {
throw new Error(`handleReceivedPostsInChannelBefore Not implemented yet. posts count${posts.length} prepareRecordsOnly=${prepareRecordsOnly}`);
}
handleReceivedPostsInChannelAfter = async (posts: Post[], prepareRecordsOnly = false): Promise<PostsInChannelModel[]> => {
throw new Error(`handleReceivedPostsInChannelAfter Not implemented yet. posts count${posts.length} prepareRecordsOnly=${prepareRecordsOnly}`);
}
handleReceivedPostForChannel = async (post: Post, prepareRecordsOnly = false): Promise<PostsInChannelModel[]> => {
throw new Error(`handleReceivedPostsInChannelAfter Not implemented yet. postId ${post.id} prepareRecordsOnly=${prepareRecordsOnly}`);
}
};
export default PostsInChannelHandler;

View File

@@ -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<string, Post[]>, prepareRecordsOnly?: boolean) => Promise<PostsInThreadModel[]>;
handleReceivedPostForThread: (post: Post, prepareRecordsOnly?: boolean) => Promise<PostsInThreadModel[]>;
}
const {POSTS_IN_THREAD} = Database.MM_TABLES.SERVER;
const PostsInThreadHandler = (superclass: any) => class extends superclass {
handleReceivedPostsInThread = async (postsMap: Record<string, Post[]>, prepareRecordsOnly = false): Promise<PostsInThreadModel[]> => {
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<PostsInThreadModel[]> => {
throw new Error(`handleReceivedPostForThread Not implemented yet. postId ${post.id} prepareRecordsOnly=${prepareRecordsOnly}`);
}
};
export default PostsInThreadHandler;

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);

View File

@@ -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<ChannelMembershipModel[]>;
handlePreferences: ({preferences, prepareRecordsOnly}: HandlePreferencesArgs) => Promise<PreferenceModel[]>;
handleReactions: ({reactions, prepareRecordsOnly}: HandleReactionsArgs) => Promise<Array<ReactionModel | CustomEmojiModel>>;
handleReactions: ({postsReactions, prepareRecordsOnly}: HandleReactionsArgs) => Promise<Array<ReactionModel | CustomEmojiModel>>;
handleUsers: ({users, prepareRecordsOnly}: HandleUsersArgs) => Promise<UserModel[]>;
}
@@ -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<Array<(ReactionModel | CustomEmojiModel)>>}
*/
handleReactions = async ({reactions, prepareRecordsOnly}: HandleReactionsArgs): Promise<Array<(ReactionModel | CustomEmojiModel)>> => {
let batchRecords: Array<ReactionModel | CustomEmojiModel> = [];
handleReactions = async ({postsReactions, prepareRecordsOnly}: HandleReactionsArgs): Promise<ReactionModel[]> => {
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;
}

View File

@@ -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,
) {

View File

@@ -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',

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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'],
},
},

View File

@@ -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;