forked from Ivasoft/mattermost-mobile
MM_33223 [v2] Database Operator - Post section (#5227)
* MM_30475 : ADDED default schema
* MM_30475 : ADDED todo for field 'value' of default/Global entity
* MM_30476 : Created schema for SERVER DB
* MM_30476 : Server model [ IN PROGRESS ]
* MM_30476 : Including types for group, groups_in_channel and role
* MM_30476 : ADDED models for Group
- @typings absolute path has been added to the tsconfig.json
* MM_30476 : ADDED typings to current models
* MM_30476 : ADDED typings to current models
* MM_30476 : ADDED models related to TEAM section of the ERD
* MM_30476 : ADDED models for User section of the ERD
* MM_30476 : ADDED models for POST section of the ERD
* MM_30476 : ADDED models for Channel section of the ERD
* MM_30475 : Updated typings and references to MM_TABLES
* MM_30476 : Verified all field names
* MM_30476 : Verified every table associations
* MM_30476 : Verified all relation fields
* MM_30476 : Updated primary id of the main models
We will override the wdb id at component level when we create a new records. This involves the models : channel, group, post, team and user.
* MM_30476 : Including 1:1 relationship amongs some entities
* MM_30476 : ADDED Schema Managers
* The migration array will hold all the migration steps.
* The initial app release (e.g. v2 )will have an empty array and subsequent releases (e.g. v2.1 ) will have the steps listed in that array.
* On initialization, the database will perform the migration to accomodate for new columns/tables creation and while it will conserve the mobile phone's data, it will also make it conform to this new schema.
* If a migration fails, the migration process will rollback any changes. This migration will be thoroughly tested in development before pushing it live.
* Revert "MM_30476 : ADDED Schema Managers"
This reverts commit a505bd5e11.
* MM_30478 : Converted schema_manager into a function
* MM_30478 : Updated schema manager and included patch for wdb
* MM_30478: Updated watermelondb patch package
* MM_30478 : Update function create_schema_manager to createSqliteAdaptorOptions
* MM_30476 : Update constant name to reflect directory name
* MM_30476 : Updated msgCount from my_channel model to message_count in server schema
* MM_30482 : Added tests for schema_manager
* MM_30482 : Database Manager [ IN PROGRESS ]
* MM_30478 : Returning an sqliteAdapter instead of an object
* MM_30476 : Apply suggestions from code review
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
* MM_30476 : Updated all imports as per instruction.
* MM_30476 : Shortening object chains by destructuring
* MM_30476 : Updated schema file structure
* MM_30476 : Prettifying @typings folder
* MM_30476 : Removing useless ids
* MM_30476 : Prettify imports for decorators
* MM_30476 : ADDED documentations and lazy queries to Channel and Channel_Info
* MM_30476 : ADDED documentations for default schema
* MM_30476 : Documentation [ IN PROGRESS ]
- Following JSDoc syntax for single line comment
- Removed redundant fields in the 'membership' tables and left only the @relation records.
* MM_30476 : Documentations [ IN PROGRESS ]
* MM_30476 : Documentations [ IN PROGRESS ]
* MM_30476 : Documentations [ IN PROGRESS ]
* MM_30476 : Documentations [ IN PROGRESS]
Updated
1) my_team and team,
2) my_channel and channel,
to each have 1:1 relationship with one another
* MM_30476 : Updated all Typescript definitions
* MM_30476 :Updated @relation to @immutableRelation
* MM_30476 : Updated description for previous_post_id
* MM_30478 : Updated patch package for wdb module
* MM_30478: DB Manager [IN PROGRESS ]
* MM_30478: DB Manager [IN PROGRESS]
* MM_30478: DB Manager [IN PROGRESS]
* MM_30478 : DB Manager [IN PROGRESS]
* MM_30478 : Deleting .db file on iOS
* MM_30478: Successfully deleting .db files and directory on iOS side
* MM_30478 : Update definition for default/global
* MM_30478 : Updated all models
* MM_30478 : Doing a bit of house cleaning
* MM_30478: Record of new server connection added to default/servers db
* TS Definitely Typed Assignment issue is now FIXED
* MM_30478 : TS Definitely Typed Assignment \n Removed all the constructors but error still in editor tabs. But this time the app is not crashing
* MM_30478 : Attempt 1 [SUCCESSFUL]
* MM_30478 : Removing useDefineForClassFields
* MM_30478 : Retrieving the servers in a list + Improved the DB Manager and Babel config
* MM_30478 : Updated babel.config.js
* MM_30478 : Minor UI correction
* MM_30478 : Jest and Typescript configuration
* MM_30478 : A bit of housekeeping
* MM_30478 : Installed WDB on Android
* MM_30478 : Deletes new server record from default DB
* MM_30478 : Returns subset of server db instances
* MM_30478 : Code clean up
* MM_30478 : Code clean up on db manager
* MM_30478 : House keeping + Patch for WDB
* MM_30478 : Android - Saving & Deleting in FilesDir [COMPLETED]
* MM_30478 : Code clean up
* MM_30478 : Code clean up
* MM_30478 : Code clean up
* MM_30478 : Test successful on Android device
* MM_30478 : Rolling back change to jest.config.js
* MM_30478 : Updated test to test_integration
* MM_30478 : Fix imports
* MM_30478 : Refactored the manual testscript
* MM_30478 : Renamed database manager test file
* MM_30478 : Code clean up
* MM_30478 : Updated manual test file with a note.
* MM_30482 : DataOperator [ IN PROGRESS ]
* MM_30482 : DataOperator - setting up the factory [ IN PROGRESS ]
* MM_30482: Code refactoring
* MM_30482 : DataOperator - setting up the factory [ IN PROGRESS ]
* MM_30482 : DataOperator - code clean up [ IN PROGRESS ]
* MM_30482 : Minor code clean up
* MM_30478 : Fixed JEST issue with TS
* MM_30478 : Fixed JEST issue with TS
* MM_30478 : Fixed JEST issue with TS
* MM_30478 : Implementing JEST test cases
* MM_30478 : Implementing JEST last test cases
* MM_30478 : Jest fixing ts errors
* MM_30478 : Database Manager Jest testing [ IN PROGRESS ]
* MM_30482 - Fixing DataOperator [ IN PROGRESS ]
* MM_30482 : Code clean up
* MM_30482 - Creates multiple records [ IN PROGRESS ]
* MM_30482 - Creates multiple records [ IN PROGRESS ]
* MM_30482 : Update operation [ COMPLETED ]
* MM_30482 : Code clean up
* MM_30482 : Updated TS for Data Operator
* Update mobile v2 detox deps
* MM_30482 : Added factories for all isolated tables
* MM_30482 : Refactored TS
* MM_30482 : Refactored base factory
* MM_30482 : Updated JSDoc for operateBaseRecord - Delete CASE
* MM_30482 : Implementing test for Data Operator
* MM_30482 : Completed tests for all isolated tables
* MM_30482 : Renamed entity_factory into operators
* MM_30482 : Fix all imports
* MM_30482 : Update multiple records
* MM_30482 : Edge case for existing records ( update instead of create )
* MM_30482 : Edge case - create instead of update
* MM_30482 : Code clean up
* MM_30482 : Code clean up
* MM_30482 : Code clean up
* MM_30482 : Code clean up
* Update app/database/admin/data_operator/operators.ts
Co-authored-by: Joseph Baylon <joseph.baylon@mattermost.com>
* Update app/database/admin/data_operator/operators.ts
Co-authored-by: Joseph Baylon <joseph.baylon@mattermost.com>
* Update app/database/admin/data_operator/operators.ts
Co-authored-by: Joseph Baylon <joseph.baylon@mattermost.com>
* MM_30482 : Imposing usage of correct table name for isolated entities
* MM_30482 : Code improvement as per Joseph reviews
* MM_30482 : Updated tests to validate choice of operator service wrt tableName
* MM_30482 : Updated PR as per suggestions
* MM_30482 : Updated comments to follow jsdoc conventions
* MM_33223 : Renamed DBInstance to DatabaseInstance
* MM_33223 : ADDED Prettier
* MM_33223 - Prettier formatting
* MM_33223 : Prettier formatting
* MM_33223 - Post section [ in progress ]
* MM_33223 : PostsInThread [99% completed ]
* MM_33223: Reaction entity completed
* MM_33223: Added Reaction to the Post
* MM_33223 : Refactored reactions utils
* MM_33223 : Added previous post id to all posts
* MM_33223 : Added File Metadata
* MM_33223 : Code clean up
* MM_33223 : Added PostMetadata
* MM_33223 : Added Draft
* MM_33223 - Removed Prettier
* MM_33223 - Undo files changes due to Prettier
* MM_33223 : Making use of MM eslint plugins
* MM_33223 : PostsInChannel [ IN PROGRESS ]
* MM_33223 : Including update_at in Post schema
* MM_33223: Code clean up
* MM_33223: Code clean up
* MM_33223 : Code clean up
* MM_33223: Testing Reaction [IN PROGRESS]
* MM_33223 : Updated typings for RawCustomEmoji in Reactions
* MM_33223 : Refactored DataOperator test
* MM_33223 : Jest - handleReactions - Completed
* MM_33223 : Jest - HandleDraft - Completed
* MM_33223 : Jest - HandleFiles - Completed
* MM_33223 : Refactored DataOperator-PostMetadata
* MM_33223 : Jest - HandlePostMetadata - Completed
* MM_33223 : Refactored posts into ordered and unordered
* MM_33223 : Refactoring + Jest Utils [ IN PROGRESS ]
* MM_33223 - Jest Utils - Completed
* MM_33223 : Jest - Remaining operators - Completed
* MM_33223 : Jest - Handler PostsInThread - Completed
* MM_33223 : Jest - HandlePostsInChannel - Completed
* MM_33223 : Refactored DataOperator class
* MM_33223 : DataOperator test clean up
* MM_33223 : DataOperator code clean up
* MM_33223 : Jest - HandlePosts - Completed
* MM_33223: JSDoc - Operators - Completed
* MM_33223 : Refactoring file types.ts
* MM_33223 : Refactored import statements
* MM_33223 : Added @database alias
* MM_33223 : Added missing JSDoc
* MM_33223 : Minor code clean up
* MM_33223 : Lint fixed
* MM_33223 : Disable eslint rules for Notification
* MM_33223 : Disable eslint rule for screens
* Update app/database/admin/data_operator/index.ts
Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
* Apply suggestions from code review
Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
* Apply suggestions from code review
Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
* MM_33223 : Update data_operatator as per suggestion
* Update app/database/admin/data_operator/index.ts
* MM_33223 : Removed OptType as the operator can do without it.
* MM_33223 : Code correction after review
* MM_33223 : Refactored Data Operator following reviews
* MM_33223 : Including a wrapper to DataOperator
* MM_33223 : Completing tests for wrapper
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
Co-authored-by: Avinash Lingaloo <>
Co-authored-by: Joseph Baylon <joseph.baylon@mattermost.com>
Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"extends": [
|
||||
"plugin:mattermost/react",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:mattermost/react"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"mattermost",
|
||||
"@typescript-eslint"
|
||||
"@typescript-eslint",
|
||||
"mattermost"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
@@ -46,7 +46,13 @@
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"no-underscore-dangle": "off"
|
||||
"no-underscore-dangle": "off",
|
||||
"indent": [2, 4, {"SwitchCase": 1}],
|
||||
"key-spacing": [2, {
|
||||
"singleLine": {
|
||||
"beforeColon": false,
|
||||
"afterColon": true
|
||||
}}]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
752
app/database/admin/data_operator/handlers/index.ts
Normal file
752
app/database/admin/data_operator/handlers/index.ts
Normal file
@@ -0,0 +1,752 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import DatabaseManager from '@database/admin/database_manager';
|
||||
import logger from '@nozbe/watermelondb/utils/common/logger';
|
||||
import {
|
||||
BatchOperations, DatabaseInstance,
|
||||
ExecuteRecords,
|
||||
HandleFiles,
|
||||
HandleIsolatedEntityData,
|
||||
HandlePostMetadata,
|
||||
HandlePosts,
|
||||
HandleReactions,
|
||||
PostImage,
|
||||
PrepareRecords,
|
||||
RawCustomEmoji,
|
||||
RawDraft,
|
||||
RawEmbed,
|
||||
RawFile,
|
||||
RawPost,
|
||||
RawPostMetadata,
|
||||
RawPostsInThread,
|
||||
RawReaction,
|
||||
} from '@typings/database/database';
|
||||
import File from '@typings/database/file';
|
||||
import CustomEmoji from '@typings/database/custom_emoji';
|
||||
import {IsolatedEntities} from '@typings/database/enums';
|
||||
import Post from '@typings/database/post';
|
||||
import PostMetadata from '@typings/database/post_metadata';
|
||||
import PostsInChannel from '@typings/database/posts_in_channel';
|
||||
import PostsInThread from '@typings/database/posts_in_thread';
|
||||
import Reaction from '@typings/database/reaction';
|
||||
|
||||
import DatabaseConnectionException from '../../exceptions/database_connection_exception';
|
||||
import DatabaseOperatorException from '../../exceptions/database_operator_exception';
|
||||
import {
|
||||
operateAppRecord,
|
||||
operateCustomEmojiRecord,
|
||||
operateDraftRecord,
|
||||
operateFileRecord,
|
||||
operateGlobalRecord,
|
||||
operatePostInThreadRecord,
|
||||
operatePostMetadataRecord,
|
||||
operatePostRecord,
|
||||
operatePostsInChannelRecord,
|
||||
operateReactionRecord,
|
||||
operateRoleRecord,
|
||||
operateServersRecord,
|
||||
operateSystemRecord,
|
||||
operateTermsOfServiceRecord,
|
||||
} from '../operators';
|
||||
import {createPostsChain, sanitizePosts, sanitizeReactions} from '../utils';
|
||||
|
||||
const {
|
||||
CUSTOM_EMOJI,
|
||||
DRAFT,
|
||||
FILE,
|
||||
POST,
|
||||
POST_METADATA,
|
||||
POSTS_IN_THREAD,
|
||||
POSTS_IN_CHANNEL,
|
||||
REACTION,
|
||||
} = MM_TABLES.SERVER;
|
||||
|
||||
if (!__DEV__) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
logger.silence();
|
||||
}
|
||||
|
||||
class DataOperator {
|
||||
/**
|
||||
* serverDatabase : In a multi-server configuration, this connection will be used by WebSockets and other parties to update databases other than the active one.
|
||||
* @type {DatabaseInstance}
|
||||
*/
|
||||
serverDatabase: DatabaseInstance
|
||||
|
||||
constructor(serverDatabase?: Database) {
|
||||
this.serverDatabase = serverDatabase;
|
||||
}
|
||||
|
||||
/**
|
||||
* handleIsolatedEntity: Handler responsible for the Create/Update operations on the isolated entities as described
|
||||
* by the IsolatedEntities enum
|
||||
* @param {HandleIsolatedEntityData} entityData
|
||||
* @param {IsolatedEntities} entityData.tableName
|
||||
* @param {Records} entityData.values
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
handleIsolatedEntity = async ({tableName, values}: HandleIsolatedEntityData) => {
|
||||
let recordOperator;
|
||||
|
||||
if (!values.length) {
|
||||
throw new DatabaseOperatorException(
|
||||
'An empty "values" array has been passed to the handleIsolatedEntity method',
|
||||
);
|
||||
}
|
||||
|
||||
switch (tableName) {
|
||||
case IsolatedEntities.APP: {
|
||||
recordOperator = operateAppRecord;
|
||||
break;
|
||||
}
|
||||
case IsolatedEntities.GLOBAL: {
|
||||
recordOperator = operateGlobalRecord;
|
||||
break;
|
||||
}
|
||||
case IsolatedEntities.SERVERS: {
|
||||
recordOperator = operateServersRecord;
|
||||
break;
|
||||
}
|
||||
case IsolatedEntities.CUSTOM_EMOJI: {
|
||||
recordOperator = operateCustomEmojiRecord;
|
||||
break;
|
||||
}
|
||||
case IsolatedEntities.ROLE: {
|
||||
recordOperator = operateRoleRecord;
|
||||
break;
|
||||
}
|
||||
case IsolatedEntities.SYSTEM: {
|
||||
recordOperator = operateSystemRecord;
|
||||
break;
|
||||
}
|
||||
case IsolatedEntities.TERMS_OF_SERVICE: {
|
||||
recordOperator = operateTermsOfServiceRecord;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
recordOperator = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (recordOperator) {
|
||||
await this.executeInDatabase({values, tableName, recordOperator});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* handleDraft: Handler responsible for the Create/Update operations occurring the Draft entity from the 'Server' schema
|
||||
* @param {RawDraft[]} drafts
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
handleDraft = async (drafts: RawDraft[]) => {
|
||||
if (!drafts.length) {
|
||||
throw new DatabaseOperatorException(
|
||||
'An empty "drafts" array has been passed to the handleReactions method',
|
||||
);
|
||||
}
|
||||
|
||||
await this.executeInDatabase({
|
||||
values: drafts,
|
||||
tableName: DRAFT,
|
||||
recordOperator: operateDraftRecord,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* handleReactions: Handler responsible for the Create/Update operations occurring on the Reaction entity from the 'Server' schema
|
||||
* @param {HandleReactions} handleReactions
|
||||
* @param {RawReaction[]} handleReactions.reactions
|
||||
* @param {boolean} handleReactions.prepareRowsOnly
|
||||
* @returns {Promise<[] | (Reaction | CustomEmoji)[]>}
|
||||
*/
|
||||
handleReactions = async ({reactions, prepareRowsOnly}: HandleReactions) => {
|
||||
if (!reactions.length) {
|
||||
throw new DatabaseOperatorException(
|
||||
'An empty "reactions" array has been passed to the handleReactions method',
|
||||
);
|
||||
}
|
||||
|
||||
const database = await this.getDatabase(REACTION);
|
||||
|
||||
const {
|
||||
createEmojis,
|
||||
createReactions,
|
||||
deleteReactions,
|
||||
} = await sanitizeReactions({
|
||||
database,
|
||||
post_id: reactions[0].post_id,
|
||||
rawReactions: reactions,
|
||||
});
|
||||
|
||||
let batchRecords: Model[] = [];
|
||||
|
||||
// Prepares record for model Reactions
|
||||
if (createReactions.length) {
|
||||
const reactionsRecords = ((await this.prepareRecords({
|
||||
database,
|
||||
recordOperator: operateReactionRecord,
|
||||
tableName: REACTION,
|
||||
values: createReactions,
|
||||
})) as unknown) as Reaction[];
|
||||
batchRecords = batchRecords.concat(reactionsRecords);
|
||||
}
|
||||
|
||||
// Prepares records for model CustomEmoji
|
||||
if (createEmojis.length) {
|
||||
const emojiRecords = ((await this.prepareRecords({
|
||||
database,
|
||||
recordOperator: operateCustomEmojiRecord,
|
||||
tableName: CUSTOM_EMOJI,
|
||||
values: createEmojis as RawCustomEmoji[],
|
||||
})) as unknown) as CustomEmoji[];
|
||||
batchRecords = batchRecords.concat(emojiRecords);
|
||||
}
|
||||
|
||||
batchRecords = batchRecords.concat(deleteReactions);
|
||||
|
||||
if (prepareRowsOnly) {
|
||||
return batchRecords;
|
||||
}
|
||||
|
||||
if (batchRecords?.length) {
|
||||
await this.batchOperations({
|
||||
database,
|
||||
models: batchRecords,
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* handlePosts: Handler responsible for the Create/Update operations occurring on the Post entity from the 'Server' schema
|
||||
* @param {HandlePosts} handlePosts
|
||||
* @param {string[]} handlePosts.orders
|
||||
* @param {RawPost[]} handlePosts.values
|
||||
* @param {string | undefined} handlePosts.previousPostId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
handlePosts = async ({orders, values, previousPostId}: HandlePosts) => {
|
||||
const tableName = POST;
|
||||
|
||||
// We rely on the order array; if it is empty, we stop processing
|
||||
if (!orders.length) {
|
||||
throw new DatabaseOperatorException(
|
||||
'An empty "order" array has been passed to the HandlePosts method',
|
||||
);
|
||||
}
|
||||
|
||||
let batch: Model[] = [];
|
||||
let files: RawFile[] = [];
|
||||
const postsInThread = [];
|
||||
let reactions: RawReaction[] = [];
|
||||
let emojis: RawCustomEmoji[] = [];
|
||||
const images: { images: Dictionary<PostImage>; postId: string }[] = [];
|
||||
const embeds: { embed: RawEmbed[]; postId: string }[] = [];
|
||||
|
||||
// We treat those posts who are present in the order array only
|
||||
const {orderedPosts, unOrderedPosts} = sanitizePosts({
|
||||
posts: values,
|
||||
orders,
|
||||
});
|
||||
|
||||
// We create the 'chain of posts' by linking each posts' previousId to the post before it in the order array
|
||||
const linkedRawPosts: RawPost[] = createPostsChain({
|
||||
orders,
|
||||
previousPostId: previousPostId || '',
|
||||
rawPosts: orderedPosts,
|
||||
});
|
||||
|
||||
const database = await this.getDatabase(tableName);
|
||||
|
||||
// Prepares records for batch processing onto the 'Post' entity for the server schema
|
||||
const posts = ((await this.prepareRecords({
|
||||
database,
|
||||
tableName,
|
||||
values: [...linkedRawPosts, ...unOrderedPosts],
|
||||
recordOperator: operatePostRecord,
|
||||
})) as unknown) as Post[];
|
||||
|
||||
// Appends the processed records into the final batch array
|
||||
batch = batch.concat(posts);
|
||||
|
||||
// Starts extracting information from each post to build up for related entities' data
|
||||
for (let i = 0; i < orderedPosts.length; i++) {
|
||||
const post = orderedPosts[i] as RawPost;
|
||||
|
||||
// PostsInChannel - a root post has an empty root_id value
|
||||
if (!post.root_id) {
|
||||
postsInThread.push({
|
||||
earliest: post.create_at,
|
||||
post_id: post.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (post?.metadata && Object.keys(post?.metadata).length > 0) {
|
||||
const metadata = post.metadata;
|
||||
|
||||
// Extracts reaction from post's metadata
|
||||
reactions = reactions.concat(metadata?.reactions ?? []);
|
||||
|
||||
// Extracts emojis from post's metadata
|
||||
emojis = emojis.concat(metadata?.emojis ?? []);
|
||||
|
||||
// Extracts files from post's metadata
|
||||
files = files.concat(metadata?.files ?? []);
|
||||
|
||||
// Extracts images and embeds from post's metadata
|
||||
if (metadata?.images) {
|
||||
images.push({images: metadata.images, postId: post.id});
|
||||
}
|
||||
|
||||
if (metadata?.embeds) {
|
||||
embeds.push({embed: metadata.embeds, postId: post.id});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reactions.length) {
|
||||
// calls handler for Reactions
|
||||
const postReactions = (await this.handleReactions({
|
||||
reactions,
|
||||
prepareRowsOnly: true,
|
||||
})) as Reaction[];
|
||||
batch = batch.concat(postReactions);
|
||||
}
|
||||
|
||||
if (files.length) {
|
||||
// calls handler for Files
|
||||
const postFiles = (await this.handleFiles({
|
||||
files,
|
||||
prepareRowsOnly: true,
|
||||
})) as File[];
|
||||
batch = batch.concat(postFiles);
|
||||
}
|
||||
|
||||
if (images.length || embeds.length) {
|
||||
// calls handler for postMetadata ( embeds and images )
|
||||
const postMetadata = (await this.handlePostMetadata({
|
||||
images,
|
||||
embeds,
|
||||
prepareRowsOnly: true,
|
||||
})) as PostMetadata[];
|
||||
batch = batch.concat(postMetadata);
|
||||
}
|
||||
|
||||
if (batch.length) {
|
||||
await this.batchOperations({database, models: batch});
|
||||
}
|
||||
|
||||
// LAST: calls handler for CustomEmojis, PostsInThread, PostsInChannel
|
||||
await this.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.CUSTOM_EMOJI,
|
||||
values: emojis,
|
||||
});
|
||||
|
||||
if (postsInThread.length) {
|
||||
await this.handlePostsInThread(postsInThread);
|
||||
}
|
||||
|
||||
if (orderedPosts.length) {
|
||||
await this.handlePostsInChannel(orderedPosts);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* handleFiles: Handler responsible for the Create/Update operations occurring on the File entity from the 'Server' schema
|
||||
* @param {HandleFiles} handleFiles
|
||||
* @param {RawFile[]} handleFiles.files
|
||||
* @param {boolean} handleFiles.prepareRowsOnly
|
||||
* @returns {Promise<File[] | any[]>}
|
||||
*/
|
||||
private handleFiles = async ({files, prepareRowsOnly}: HandleFiles) => {
|
||||
if (!files.length) {
|
||||
throw new DatabaseOperatorException(
|
||||
'An empty "files" array has been passed to the handleFiles method',
|
||||
);
|
||||
}
|
||||
|
||||
const database = await this.getDatabase(FILE);
|
||||
|
||||
const postFiles = ((await this.prepareRecords({
|
||||
database,
|
||||
recordOperator: operateFileRecord,
|
||||
tableName: FILE,
|
||||
values: files,
|
||||
})) as unknown) as File[];
|
||||
|
||||
if (prepareRowsOnly) {
|
||||
return postFiles;
|
||||
}
|
||||
|
||||
if (postFiles?.length) {
|
||||
await this.batchOperations({database, models: [...postFiles]});
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* handlePostMetadata: Handler responsible for the Create/Update operations occurring on the PostMetadata entity from the 'Server' schema
|
||||
* @param {HandlePostMetadata} handlePostMetadata
|
||||
* @param {{embed: RawEmbed[], postId: string}[] | undefined} handlePostMetadata.embeds
|
||||
* @param {{images: Dictionary<PostImage>, postId: string}[] | undefined} handlePostMetadata.images
|
||||
* @param {boolean} handlePostMetadata.prepareRowsOnly
|
||||
* @returns {Promise<any[] | PostMetadata[]>}
|
||||
*/
|
||||
private handlePostMetadata = async ({embeds, images, prepareRowsOnly}: HandlePostMetadata) => {
|
||||
const metadata: RawPostMetadata[] = [];
|
||||
|
||||
if (images?.length) {
|
||||
images.forEach((image) => {
|
||||
const imageEntry = Object.entries(image.images);
|
||||
metadata.push({
|
||||
data: {...imageEntry?.[0]?.[1], url: imageEntry?.[0]?.[0]},
|
||||
type: 'images',
|
||||
postId: image.postId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (embeds?.length) {
|
||||
embeds.forEach((postEmbed) => {
|
||||
postEmbed.embed.forEach((embed: RawEmbed) => {
|
||||
metadata.push({
|
||||
data: {...embed.data},
|
||||
type: embed.type,
|
||||
postId: postEmbed.postId,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!metadata.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const database = await this.getDatabase(POST_METADATA);
|
||||
|
||||
const postMetas = ((await this.prepareRecords({
|
||||
database,
|
||||
recordOperator: operatePostMetadataRecord,
|
||||
tableName: POST_METADATA,
|
||||
values: metadata,
|
||||
})) as unknown) as PostMetadata[];
|
||||
|
||||
if (prepareRowsOnly) {
|
||||
return postMetas;
|
||||
}
|
||||
|
||||
if (postMetas?.length) {
|
||||
await this.batchOperations({database, models: [...postMetas]});
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* handlePostsInThread: Handler responsible for the Create/Update operations occurring on the PostsInThread entity from the 'Server' schema
|
||||
* @param {RawPostsInThread[]} rootPosts
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private handlePostsInThread = async (rootPosts: RawPostsInThread[]) => {
|
||||
if (!rootPosts.length) {
|
||||
throw new DatabaseOperatorException(
|
||||
'An empty "rootPosts" array has been passed to the handlePostsInThread method',
|
||||
);
|
||||
}
|
||||
|
||||
// Creates an array of post ids
|
||||
const postIds = rootPosts.map((postThread) => postThread.post_id);
|
||||
|
||||
const rawPostsInThreads: RawPostsInThread[] = [];
|
||||
|
||||
const database = await this.getDatabase(POSTS_IN_THREAD);
|
||||
|
||||
// Retrieves all threads whereby their root_id can be one of the element in the postIds array
|
||||
const threads = (await database.collections.
|
||||
get(POST).
|
||||
query(Q.where('root_id', Q.oneOf(postIds))).
|
||||
fetch()) as Post[];
|
||||
|
||||
// The aim here is to find the last reply in that thread; hence the latest create_at value
|
||||
rootPosts.forEach((rootPost) => {
|
||||
const childPosts = [];
|
||||
let maxCreateAt = 0;
|
||||
for (let i = 0; i < threads.length; i++) {
|
||||
const thread = threads[i];
|
||||
if (thread?.rootId === rootPost.post_id) {
|
||||
// Creates a sub-array of threads relating to rootPost.post_id
|
||||
childPosts.push(thread);
|
||||
}
|
||||
|
||||
// Retrieves max createAt date of all posts whose root_id is rootPost.post_id
|
||||
maxCreateAt = thread.createAt > maxCreateAt ? thread.createAt : maxCreateAt;
|
||||
}
|
||||
|
||||
// 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({
|
||||
database,
|
||||
recordOperator: operatePostInThreadRecord,
|
||||
tableName: POSTS_IN_THREAD,
|
||||
values: rawPostsInThreads,
|
||||
})) as unknown) as PostsInThread[];
|
||||
|
||||
if (postInThreadRecords?.length) {
|
||||
await this.batchOperations({database, models: postInThreadRecords});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* handlePostsInChannel: Handler responsible for the Create/Update operations occurring on the PostsInChannel entity from the 'Server' schema
|
||||
* @param {RawPost[]} posts
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private handlePostsInChannel = async (posts: RawPost[]) => {
|
||||
// At this point, the parameter 'posts' is already a chain of posts. Now, we have to figure out how to plug it
|
||||
// into existing chains in the PostsInChannel table
|
||||
if (!posts.length) {
|
||||
throw new DatabaseOperatorException(
|
||||
'An empty "posts" array has been passed to the handlePostsInChannel method',
|
||||
);
|
||||
}
|
||||
|
||||
// Sort a clone of 'posts' array by create_at ( oldest to newest )
|
||||
const sortedPosts = [...posts].sort((a, b) => {
|
||||
return a.create_at - b.create_at;
|
||||
});
|
||||
|
||||
// The first element (beginning of chain)
|
||||
const tipOfChain: RawPost = sortedPosts[0];
|
||||
|
||||
// Channel Id for this chain of posts
|
||||
const channelId = tipOfChain.channel_id;
|
||||
|
||||
// Find smallest 'create_at' value in chain
|
||||
const earliest = tipOfChain.create_at;
|
||||
|
||||
// Find highest 'create_at' value in chain
|
||||
const latest = sortedPosts[sortedPosts.length - 1].create_at;
|
||||
|
||||
const database = await this.getDatabase(POSTS_IN_CHANNEL);
|
||||
|
||||
// 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 createPostsInChannelRecord = async () => {
|
||||
await this.executeInDatabase({
|
||||
values: [{channel_id: channelId, earliest, latest}],
|
||||
tableName: POSTS_IN_CHANNEL,
|
||||
recordOperator: operatePostsInChannelRecord,
|
||||
});
|
||||
};
|
||||
|
||||
// chunk length 0; then it's a new chunk to be added to the PostsInChannel table
|
||||
if (chunks.length === 0) {
|
||||
await createPostsInChannelRecord();
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort chunks (in-place) by earliest field ( oldest to newest )
|
||||
chunks.sort((a, b) => {
|
||||
return a.earliest - b.earliest;
|
||||
});
|
||||
|
||||
let found = false;
|
||||
let targetChunk: PostsInChannel;
|
||||
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
||||
// find if we should plug the chain before
|
||||
const chunk = chunks[chunkIndex];
|
||||
if (earliest < chunk.earliest) {
|
||||
found = true;
|
||||
targetChunk = chunk;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
// We have a potential chunk to plug nearby
|
||||
const potentialPosts = (await database.collections.
|
||||
get(POST).
|
||||
query(Q.where('create_at', earliest)).
|
||||
fetch()) as Post[];
|
||||
|
||||
if (potentialPosts?.length > 0) {
|
||||
const targetPost = potentialPosts[0];
|
||||
|
||||
// now we decide if we need to operate on the targetChunk or just create a new chunk
|
||||
const isChainable = tipOfChain.prev_post_id === targetPost.previousPostId;
|
||||
|
||||
if (isChainable) {
|
||||
// Update this chunk's data in PostsInChannel table. earliest comes from tipOfChain while latest comes from chunk
|
||||
await database.action(async () => {
|
||||
await targetChunk.update((postInChannel) => {
|
||||
postInChannel.earliest = earliest;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
await createPostsInChannelRecord();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await createPostsInChannelRecord();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* batchOperations: Accepts an instance of Database (either Default or Server) and an array of
|
||||
* prepareCreate/prepareUpdate 'models' and executes the actions on the database.
|
||||
* @param {BatchOperations} operation
|
||||
* @param {Database} operation.database
|
||||
* @param {Array} operation.models
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private batchOperations = async ({database, models}: BatchOperations) => {
|
||||
try {
|
||||
if (models.length > 0) {
|
||||
await database.action(async () => {
|
||||
await database.batch(...models);
|
||||
});
|
||||
} else {
|
||||
throw new DatabaseOperatorException(
|
||||
'batchOperations does not process empty model array',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new DatabaseOperatorException('batchOperations error ', e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* prepareRecords: This method loops over the raw data, onto which it calls the assigned operator finally produce an array of prepareUpdate/prepareCreate objects
|
||||
* @param {PrepareRecords} prepareRecords
|
||||
* @param {Database} prepareRecords.database
|
||||
* @param {RecordValue[]} prepareRecords.values
|
||||
* @param {RecordOperator} prepareRecords.recordOperator
|
||||
* @returns {Promise<Model[]>}
|
||||
*/
|
||||
private prepareRecords = async ({database, values, recordOperator}: PrepareRecords) => {
|
||||
if (!values.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!Array.isArray(values) || !values?.length || !database) {
|
||||
throw new DatabaseOperatorException(
|
||||
'prepareRecords accepts only values of type RecordValue[] or valid database connection',
|
||||
);
|
||||
}
|
||||
|
||||
const recordPromises = await values.map(async (value) => {
|
||||
const record = await recordOperator({database, value});
|
||||
return record;
|
||||
});
|
||||
|
||||
const results = await Promise.all(recordPromises);
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* executeInDatabase: This method uses the prepare records from the 'prepareRecords' method and send them as one transaction to the database.
|
||||
* @param {ExecuteRecords} executor
|
||||
* @param {RecordOperator} executor.recordOperator
|
||||
* @param {string} executor.tableName
|
||||
* @param {RecordValue[]} executor.values
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private executeInDatabase = async ({recordOperator, tableName, values}: ExecuteRecords) => {
|
||||
if (!values.length) {
|
||||
throw new DatabaseOperatorException(
|
||||
'An empty "values" array has been passed to the executeInDatabase method',
|
||||
);
|
||||
}
|
||||
|
||||
const database = await this.getDatabase(tableName);
|
||||
|
||||
const models = ((await this.prepareRecords({
|
||||
database,
|
||||
recordOperator,
|
||||
tableName,
|
||||
values,
|
||||
})) as unknown) as Model[];
|
||||
|
||||
if (models?.length > 0) {
|
||||
await this.batchOperations({database, models});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* getDatabase: Based on the table's name, it will return a database instance either from the 'DEFAULT' database or
|
||||
* the 'SERVER' database
|
||||
* @param {string} tableName
|
||||
* @returns {Promise<Database>}
|
||||
*/
|
||||
private getDatabase = async (tableName: string) => {
|
||||
const isDefaultConnection = Object.values(MM_TABLES.DEFAULT).some(
|
||||
(tbName) => {
|
||||
return tableName === tbName;
|
||||
},
|
||||
);
|
||||
|
||||
const promise = isDefaultConnection ? this.getDefaultDatabase : this.getServerDatabase;
|
||||
const connection = await promise();
|
||||
|
||||
return connection;
|
||||
};
|
||||
|
||||
/**
|
||||
* getDefaultDatabase: Returns the default database
|
||||
* @returns {Promise<Database>}
|
||||
*/
|
||||
private getDefaultDatabase = async () => {
|
||||
const connection = await DatabaseManager.getDefaultDatabase();
|
||||
if (connection === undefined) {
|
||||
throw new DatabaseConnectionException(
|
||||
'An error occurred while retrieving the default database',
|
||||
'',
|
||||
);
|
||||
}
|
||||
return connection;
|
||||
};
|
||||
|
||||
/**
|
||||
* getServerDatabase: Returns the current active server database or if a connection is passed to the constructor, it will return that one.
|
||||
* @returns {Promise<Database>}
|
||||
*/
|
||||
private getServerDatabase = async () => {
|
||||
// Third parties trying to update the database
|
||||
if (this.serverDatabase) {
|
||||
return this.serverDatabase;
|
||||
}
|
||||
|
||||
// NOTE: here we are getting the active server directly as in a multi-server support system, the current
|
||||
// active server connection will already be set on application init
|
||||
const connection = await DatabaseManager.getActiveServerDatabase();
|
||||
if (connection === undefined) {
|
||||
throw new DatabaseConnectionException(
|
||||
'An error occurred while retrieving the server database',
|
||||
'',
|
||||
);
|
||||
}
|
||||
return connection;
|
||||
};
|
||||
}
|
||||
|
||||
export default DataOperator;
|
||||
641
app/database/admin/data_operator/handlers/test.ts
Normal file
641
app/database/admin/data_operator/handlers/test.ts
Normal file
@@ -0,0 +1,641 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import DatabaseManager from '@database/admin/database_manager';
|
||||
import DataOperator from '@database/admin/data_operator';
|
||||
import {DatabaseType, IsolatedEntities} from '@typings/database/enums';
|
||||
|
||||
import {
|
||||
operateAppRecord,
|
||||
operateCustomEmojiRecord,
|
||||
operateDraftRecord,
|
||||
operateGlobalRecord,
|
||||
operateRoleRecord,
|
||||
operateServersRecord,
|
||||
operateSystemRecord,
|
||||
operateTermsOfServiceRecord,
|
||||
} from '../operators';
|
||||
|
||||
jest.mock('@database/admin/database_manager');
|
||||
|
||||
const {DRAFT} = MM_TABLES.SERVER;
|
||||
|
||||
describe('*** DataOperator: Handlers tests ***', () => {
|
||||
const createConnection = async (setActive = false) => {
|
||||
const dbName = 'server_schema_connection';
|
||||
const serverUrl = 'https://appv2.mattermost.com';
|
||||
const database = await DatabaseManager.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl,
|
||||
},
|
||||
});
|
||||
|
||||
if (setActive) {
|
||||
await DatabaseManager.setActiveServerDatabase({
|
||||
displayName: dbName,
|
||||
serverUrl,
|
||||
});
|
||||
}
|
||||
|
||||
return database;
|
||||
};
|
||||
|
||||
it('=> HandleApp: should write to APP entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'executeInDatabase');
|
||||
|
||||
const data = {
|
||||
tableName: IsolatedEntities.APP,
|
||||
values: [
|
||||
{
|
||||
buildNumber: 'build-10x',
|
||||
createdAt: 1,
|
||||
id: 'id-21',
|
||||
versionNumber: 'version-10',
|
||||
},
|
||||
{
|
||||
buildNumber: 'build-11y',
|
||||
createdAt: 1,
|
||||
id: 'id-22',
|
||||
versionNumber: 'version-11',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await DataOperator.handleIsolatedEntity(data);
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledWith({
|
||||
...data,
|
||||
recordOperator: operateAppRecord,
|
||||
});
|
||||
});
|
||||
|
||||
it('=> HandleGlobal: should write to GLOBAL entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'executeInDatabase');
|
||||
|
||||
const data = {
|
||||
tableName: IsolatedEntities.GLOBAL,
|
||||
values: [
|
||||
{id: 'global-1-id', name: 'global-1-name', value: 'global-1-value'},
|
||||
],
|
||||
};
|
||||
|
||||
await DataOperator.handleIsolatedEntity(data);
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledWith({
|
||||
...data,
|
||||
recordOperator: operateGlobalRecord,
|
||||
});
|
||||
});
|
||||
|
||||
it('=> HandleServers: should write to SERVERS entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'executeInDatabase');
|
||||
|
||||
const data = {
|
||||
tableName: IsolatedEntities.SERVERS,
|
||||
values: [
|
||||
{
|
||||
dbPath: 'server.db',
|
||||
displayName: 'community',
|
||||
id: 'server-id-1',
|
||||
mentionCount: 0,
|
||||
unreadCount: 0,
|
||||
url: 'https://community.mattermost.com',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await DataOperator.handleIsolatedEntity(data);
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledWith({
|
||||
...data,
|
||||
recordOperator: operateServersRecord,
|
||||
});
|
||||
});
|
||||
|
||||
it('=> HandleCustomEmoji: should write to CUSTOM_EMOJI entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const database = await createConnection(true);
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'executeInDatabase');
|
||||
|
||||
const data = {
|
||||
tableName: IsolatedEntities.CUSTOM_EMOJI,
|
||||
values: [
|
||||
{
|
||||
id: 'custom-emoji-id-1',
|
||||
name: 'custom-emoji-1',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await DataOperator.handleIsolatedEntity(data);
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledWith({
|
||||
...data,
|
||||
recordOperator: operateCustomEmojiRecord,
|
||||
});
|
||||
});
|
||||
|
||||
it('=> HandleRole: should write to ROLE entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const database = await createConnection(true);
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'executeInDatabase');
|
||||
|
||||
const data = {
|
||||
tableName: IsolatedEntities.ROLE,
|
||||
values: [
|
||||
{
|
||||
id: 'custom-emoji-id-1',
|
||||
name: 'custom-emoji-1',
|
||||
permissions: ['custom-emoji-1'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await DataOperator.handleIsolatedEntity(data);
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledWith({
|
||||
...data,
|
||||
recordOperator: operateRoleRecord,
|
||||
});
|
||||
});
|
||||
|
||||
it('=> HandleSystem: should write to SYSTEM entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const database = await createConnection(true);
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'executeInDatabase');
|
||||
|
||||
const data = {
|
||||
tableName: IsolatedEntities.SYSTEM,
|
||||
values: [{id: 'system-id-1', name: 'system-1', value: 'system-1'}],
|
||||
};
|
||||
|
||||
await DataOperator.handleIsolatedEntity(data);
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledWith({
|
||||
...data,
|
||||
recordOperator: operateSystemRecord,
|
||||
});
|
||||
});
|
||||
|
||||
it('=> HandleTermsOfService: should write to TERMS_OF_SERVICE entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const database = await createConnection(true);
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'executeInDatabase');
|
||||
|
||||
const data = {
|
||||
tableName: IsolatedEntities.TERMS_OF_SERVICE,
|
||||
values: [{id: 'tos-1', acceptedAt: 1}],
|
||||
};
|
||||
|
||||
await DataOperator.handleIsolatedEntity(data);
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledWith({
|
||||
...data,
|
||||
recordOperator: operateTermsOfServiceRecord,
|
||||
});
|
||||
});
|
||||
|
||||
it('=> No table name: should not call executeInDatabase if tableName is invalid', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'executeInDatabase');
|
||||
|
||||
await DataOperator.handleIsolatedEntity({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
tableName: 'INVALID_TABLE_NAME',
|
||||
values: [{id: 'tos-1', acceptedAt: 1}],
|
||||
});
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('=> HandleReactions: should write to both Reactions and CustomEmoji entities', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createConnection(true);
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const spyOnPrepareBase = jest.spyOn(DataOperator as any, 'prepareRecords');
|
||||
const spyOnBatchOperation = jest.spyOn(DataOperator as any, 'batchOperations');
|
||||
|
||||
await DataOperator.handleReactions({
|
||||
reactions: [
|
||||
{
|
||||
create_at: 1608263728086,
|
||||
delete_at: 0,
|
||||
emoji_name: 'p4p1',
|
||||
post_id: '4r9jmr7eqt8dxq3f9woypzurry',
|
||||
update_at: 1608263728077,
|
||||
user_id: 'ooumoqgq3bfiijzwbn8badznwc',
|
||||
},
|
||||
],
|
||||
prepareRowsOnly: false,
|
||||
});
|
||||
|
||||
// Called twice: Once for Reaction record and once for CustomEmoji record
|
||||
expect(spyOnPrepareBase).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Only one batch operation for both entities
|
||||
expect(spyOnBatchOperation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('=> HandleDraft: should write to the Draft entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const database = await createConnection(true);
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'executeInDatabase');
|
||||
|
||||
const data = [
|
||||
{
|
||||
channel_id: '4r9jmr7eqt8dxq3f9woypzurrychannelid',
|
||||
files: [
|
||||
{
|
||||
user_id: 'user_id',
|
||||
post_id: 'post_id',
|
||||
create_at: 123,
|
||||
update_at: 456,
|
||||
delete_at: 789,
|
||||
name: 'an_image',
|
||||
extension: 'jpg',
|
||||
size: 10,
|
||||
mime_type: 'image',
|
||||
width: 10,
|
||||
height: 10,
|
||||
has_preview_image: false,
|
||||
clientId: 'clientId',
|
||||
},
|
||||
],
|
||||
message: 'test draft message for post',
|
||||
root_id: '',
|
||||
},
|
||||
];
|
||||
await DataOperator.handleDraft(data);
|
||||
|
||||
// Only one batch operation for both entities
|
||||
expect(spyOnHandleBase).toHaveBeenCalledWith({
|
||||
tableName: DRAFT,
|
||||
values: data,
|
||||
recordOperator: operateDraftRecord,
|
||||
});
|
||||
});
|
||||
|
||||
it('=> HandleFiles: should write to File entity', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createConnection(true);
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const spyOnPrepareBase = jest.spyOn(DataOperator as any, 'prepareRecords');
|
||||
const spyOnBatchOperation = jest.spyOn(DataOperator as any, 'batchOperations');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
await DataOperator.handleFiles({
|
||||
files: [
|
||||
{
|
||||
user_id: 'user_id',
|
||||
post_id: 'post_id',
|
||||
create_at: 12345,
|
||||
update_at: 456,
|
||||
delete_at: 789,
|
||||
name: 'an_image',
|
||||
extension: 'jpg',
|
||||
size: 10,
|
||||
mime_type: 'image',
|
||||
width: 10,
|
||||
height: 10,
|
||||
has_preview_image: false,
|
||||
},
|
||||
],
|
||||
prepareRowsOnly: false,
|
||||
});
|
||||
|
||||
expect(spyOnPrepareBase).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnBatchOperation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('=> HandlePosts: should write to Post and its sub-child entities', async () => {
|
||||
expect.assertions(12);
|
||||
|
||||
const posts = [
|
||||
{
|
||||
id: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
create_at: 1596032651747,
|
||||
update_at: 1596032651747,
|
||||
edit_at: 0,
|
||||
delete_at: 0,
|
||||
is_pinned: false,
|
||||
user_id: 'q3mzxua9zjfczqakxdkowc6u6yy',
|
||||
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
|
||||
root_id: '',
|
||||
parent_id: 'ps81iqbddesfby8jayz7owg4yypoo',
|
||||
original_id: '',
|
||||
message: "I'll second these kudos! Thanks m!",
|
||||
type: '',
|
||||
props: {},
|
||||
hashtags: '',
|
||||
pending_post_id: '',
|
||||
reply_count: 4,
|
||||
last_reply_at: 0,
|
||||
participants: null,
|
||||
metadata: {
|
||||
images: {
|
||||
'https://community-release.mattermost.com/api/v4/image?url=https%3A%2F%2Favatars1.githubusercontent.com%2Fu%2F6913320%3Fs%3D400%26v%3D4': {
|
||||
width: 400,
|
||||
height: 400,
|
||||
format: 'png',
|
||||
frame_count: 0,
|
||||
},
|
||||
},
|
||||
reactions: [
|
||||
{
|
||||
user_id: 'njic1w1k5inefp848jwk6oukio',
|
||||
post_id: 'a7ebyw883trm884p1qcgt8yw4a',
|
||||
emoji_name: 'clap',
|
||||
create_at: 1608252965442,
|
||||
update_at: 1608252965442,
|
||||
delete_at: 0,
|
||||
},
|
||||
],
|
||||
embeds: [
|
||||
{
|
||||
type: 'opengraph',
|
||||
url:
|
||||
'https://github.com/mickmister/mattermost-plugin-default-theme',
|
||||
data: {
|
||||
type: 'object',
|
||||
url:
|
||||
'https://github.com/mickmister/mattermost-plugin-default-theme',
|
||||
title: 'mickmister/mattermost-plugin-default-theme',
|
||||
description:
|
||||
'Contribute to mickmister/mattermost-plugin-default-theme development by creating an account on GitHub.',
|
||||
determiner: '',
|
||||
site_name: 'GitHub',
|
||||
locale: '',
|
||||
locales_alternate: null,
|
||||
images: [
|
||||
{
|
||||
url: '',
|
||||
secure_url:
|
||||
'https://community-release.mattermost.com/api/v4/image?url=https%3A%2F%2Favatars1.githubusercontent.com%2Fu%2F6913320%3Fs%3D400%26v%3D4',
|
||||
type: '',
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
],
|
||||
audios: null,
|
||||
videos: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
emojis: [
|
||||
{
|
||||
id: 'dgwyadacdbbwjc8t357h6hwsrh',
|
||||
create_at: 1502389307432,
|
||||
update_at: 1502389307432,
|
||||
delete_at: 0,
|
||||
creator_id: 'x6sdh1ok1tyd9f4dgq4ybw839a',
|
||||
name: 'thanks',
|
||||
},
|
||||
],
|
||||
files: [
|
||||
{
|
||||
id: 'f1oxe5rtepfs7n3zifb4sso7po',
|
||||
user_id: '89ertha8xpfsumpucqppy5knao',
|
||||
post_id: 'a7ebyw883trm884p1qcgt8yw4a',
|
||||
create_at: 1608270920357,
|
||||
update_at: 1608270920357,
|
||||
delete_at: 0,
|
||||
name: '4qtwrg.jpg',
|
||||
extension: 'jpg',
|
||||
size: 89208,
|
||||
mime_type: 'image/jpeg',
|
||||
width: 500,
|
||||
height: 656,
|
||||
has_preview_image: true,
|
||||
mini_preview:
|
||||
'/9j/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABAAEAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AN/T/iZp+pX15FpUmnwLbXtpJpyy2sQLw8CcBXA+bksCDnHGOaf4W+P3xIshbQ6loB8RrbK11f3FpbBFW3ZwiFGHB2kr25BIOeCPPbX4S3407T7rTdDfxFNIpDyRaw9lsB4OECHGR15yO4GK6fRPhR4sGmSnxAs8NgchNOjvDPsjz8qSHA37cDk5JPPFdlOpTdPlcVt/Ku1lrvr17b67EPnjrH8/626H/9k=',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '8fcnk3p1jt8mmkaprgajoxz115a',
|
||||
create_at: 1596104683748,
|
||||
update_at: 1596104683748,
|
||||
edit_at: 0,
|
||||
delete_at: 0,
|
||||
is_pinned: false,
|
||||
user_id: 'hy5sq51sebfh58ktrce5ijtcwyy',
|
||||
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
|
||||
root_id: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
parent_id: '',
|
||||
original_id: '',
|
||||
message: 'a added to the channel by j.',
|
||||
type: 'system_add_to_channel',
|
||||
props: {
|
||||
addedUserId: 'z89qsntet7bimd3xddfu7u9ncdaxc',
|
||||
addedUsername: 'a',
|
||||
userId: 'hy5sdfdfq51sebfh58ktrce5ijtcwy',
|
||||
username: 'j',
|
||||
},
|
||||
hashtags: '',
|
||||
pending_post_id: '',
|
||||
reply_count: 0,
|
||||
last_reply_at: 0,
|
||||
participants: null,
|
||||
metadata: {},
|
||||
},
|
||||
{
|
||||
id: '3y3w3a6gkbg73bnj3xund9o5ic',
|
||||
create_at: 1596277483749,
|
||||
update_at: 1596277483749,
|
||||
edit_at: 0,
|
||||
delete_at: 0,
|
||||
is_pinned: false,
|
||||
user_id: '44ud4m9tqwby3mphzzdwm7h31sr',
|
||||
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
|
||||
root_id: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
parent_id: 'ps81iqbwesfby8jayz7owg4yypo',
|
||||
original_id: '',
|
||||
message: 'Great work M!',
|
||||
type: '',
|
||||
props: {},
|
||||
hashtags: '',
|
||||
pending_post_id: '',
|
||||
reply_count: 4,
|
||||
last_reply_at: 0,
|
||||
participants: null,
|
||||
metadata: {},
|
||||
},
|
||||
];
|
||||
|
||||
const spyOnHandleReactions = jest.spyOn(DataOperator as any, 'handleReactions');
|
||||
const spyOnHandleFiles = jest.spyOn(DataOperator as any, 'handleFiles');
|
||||
const spyOnHandlePostMetadata = jest.spyOn(DataOperator as any, 'handlePostMetadata');
|
||||
const spyOnHandleIsolatedEntity = jest.spyOn(DataOperator as any, 'handleIsolatedEntity');
|
||||
const spyOnHandlePostsInThread = jest.spyOn(DataOperator as any, 'handlePostsInThread');
|
||||
const spyOnHandlePostsInChannel = jest.spyOn(DataOperator as any, 'handlePostsInChannel');
|
||||
|
||||
await createConnection(true);
|
||||
|
||||
// handlePosts will in turn call handlePostsInThread
|
||||
await DataOperator.handlePosts({
|
||||
orders: [
|
||||
'8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
'8fcnk3p1jt8mmkaprgajoxz115a',
|
||||
'3y3w3a6gkbg73bnj3xund9o5ic',
|
||||
],
|
||||
values: posts,
|
||||
previousPostId: '',
|
||||
});
|
||||
|
||||
expect(spyOnHandleReactions).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnHandleReactions).toHaveBeenCalledWith({
|
||||
reactions: [
|
||||
{
|
||||
user_id: 'njic1w1k5inefp848jwk6oukio',
|
||||
post_id: 'a7ebyw883trm884p1qcgt8yw4a',
|
||||
emoji_name: 'clap',
|
||||
create_at: 1608252965442,
|
||||
update_at: 1608252965442,
|
||||
delete_at: 0,
|
||||
},
|
||||
],
|
||||
prepareRowsOnly: true,
|
||||
});
|
||||
|
||||
expect(spyOnHandleFiles).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnHandleFiles).toHaveBeenCalledWith({
|
||||
files: [
|
||||
{
|
||||
id: 'f1oxe5rtepfs7n3zifb4sso7po',
|
||||
user_id: '89ertha8xpfsumpucqppy5knao',
|
||||
post_id: 'a7ebyw883trm884p1qcgt8yw4a',
|
||||
create_at: 1608270920357,
|
||||
update_at: 1608270920357,
|
||||
delete_at: 0,
|
||||
name: '4qtwrg.jpg',
|
||||
extension: 'jpg',
|
||||
size: 89208,
|
||||
mime_type: 'image/jpeg',
|
||||
width: 500,
|
||||
height: 656,
|
||||
has_preview_image: true,
|
||||
mini_preview:
|
||||
'/9j/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABAAEAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AN/T/iZp+pX15FpUmnwLbXtpJpyy2sQLw8CcBXA+bksCDnHGOaf4W+P3xIshbQ6loB8RrbK11f3FpbBFW3ZwiFGHB2kr25BIOeCPPbX4S3407T7rTdDfxFNIpDyRaw9lsB4OECHGR15yO4GK6fRPhR4sGmSnxAs8NgchNOjvDPsjz8qSHA37cDk5JPPFdlOpTdPlcVt/Ku1lrvr17b67EPnjrH8/626H/9k=',
|
||||
},
|
||||
],
|
||||
prepareRowsOnly: true,
|
||||
});
|
||||
|
||||
expect(spyOnHandlePostMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnHandlePostMetadata).toHaveBeenCalledWith({
|
||||
embeds: [
|
||||
{
|
||||
embed: [
|
||||
{
|
||||
type: 'opengraph',
|
||||
url: 'https://github.com/mickmister/mattermost-plugin-default-theme',
|
||||
data: {
|
||||
type: 'object',
|
||||
url: 'https://github.com/mickmister/mattermost-plugin-default-theme',
|
||||
title: 'mickmister/mattermost-plugin-default-theme',
|
||||
description: 'Contribute to mickmister/mattermost-plugin-default-theme development by creating an account on GitHub.',
|
||||
determiner: '',
|
||||
site_name: 'GitHub',
|
||||
locale: '',
|
||||
locales_alternate: null,
|
||||
images: [
|
||||
{
|
||||
url: '',
|
||||
secure_url: 'https://community-release.mattermost.com/api/v4/image?url=https%3A%2F%2Favatars1.githubusercontent.com%2Fu%2F6913320%3Fs%3D400%26v%3D4',
|
||||
type: '',
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
],
|
||||
audios: null,
|
||||
videos: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
postId: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
},
|
||||
],
|
||||
images: [
|
||||
{
|
||||
images: {
|
||||
'https://community-release.mattermost.com/api/v4/image?url=https%3A%2F%2Favatars1.githubusercontent.com%2Fu%2F6913320%3Fs%3D400%26v%3D4': {
|
||||
width: 400,
|
||||
height: 400,
|
||||
format: 'png',
|
||||
frame_count: 0,
|
||||
},
|
||||
},
|
||||
postId: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
},
|
||||
],
|
||||
prepareRowsOnly: true,
|
||||
});
|
||||
|
||||
expect(spyOnHandleIsolatedEntity).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnHandleIsolatedEntity).toHaveBeenCalledWith({
|
||||
tableName: 'CustomEmoji',
|
||||
values: [
|
||||
{
|
||||
id: 'dgwyadacdbbwjc8t357h6hwsrh',
|
||||
create_at: 1502389307432,
|
||||
update_at: 1502389307432,
|
||||
delete_at: 0,
|
||||
creator_id: 'x6sdh1ok1tyd9f4dgq4ybw839a',
|
||||
name: 'thanks',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(spyOnHandlePostsInThread).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnHandlePostsInThread).toHaveBeenCalledWith([{earliest: 1596032651747, post_id: '8swgtrrdiff89jnsiwiip3y1eoe'}]);
|
||||
|
||||
expect(spyOnHandlePostsInChannel).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnHandlePostsInChannel).toHaveBeenCalledWith(posts.slice(0, 3));
|
||||
});
|
||||
});
|
||||
@@ -1,183 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {
|
||||
BatchOperations,
|
||||
DBInstance,
|
||||
HandleBaseData,
|
||||
HandleIsolatedEntityData,
|
||||
RecordValue,
|
||||
} from '@typings/database/database';
|
||||
|
||||
import DatabaseManager from '../database_manager';
|
||||
|
||||
import {
|
||||
operateAppRecord,
|
||||
operateCustomEmojiRecord,
|
||||
operateGlobalRecord,
|
||||
operateRoleRecord,
|
||||
operateServersRecord,
|
||||
operateSystemRecord,
|
||||
operateTermsOfServiceRecord,
|
||||
} from './operators';
|
||||
|
||||
export enum OperationType {
|
||||
CREATE = 'CREATE',
|
||||
UPDATE = 'UPDATE',
|
||||
DELETE = 'DELETE'
|
||||
}
|
||||
|
||||
export enum IsolatedEntities {
|
||||
APP= 'app',
|
||||
GLOBAL = 'global',
|
||||
SERVERS = 'servers',
|
||||
CUSTOM_EMOJI = 'CustomEmoji',
|
||||
ROLE = 'Role',
|
||||
SYSTEM = 'System',
|
||||
TERMS_OF_SERVICE = 'TermsOfService'
|
||||
}
|
||||
|
||||
class DataOperator {
|
||||
private defaultDatabase: DBInstance;
|
||||
private serverDatabase: DBInstance;
|
||||
|
||||
/**
|
||||
* handleIsolatedEntityData: Operator that handles Create/Update operations on the isolated entities as
|
||||
* described by the IsolatedTables type
|
||||
* @param {HandleIsolatedEntityData} entityData
|
||||
* @param {OperationType} entityData.optType
|
||||
* @param {IsolatedEntities} entityData.tableName
|
||||
* @param {Records} entityData.values
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
handleIsolatedEntityData = async ({optType, tableName, values}: HandleIsolatedEntityData): Promise<void> => {
|
||||
let recordOperator;
|
||||
|
||||
switch (tableName) {
|
||||
case IsolatedEntities.APP : {
|
||||
recordOperator = operateAppRecord;
|
||||
break;
|
||||
}
|
||||
case IsolatedEntities.GLOBAL : {
|
||||
recordOperator = operateGlobalRecord;
|
||||
break;
|
||||
}
|
||||
case IsolatedEntities.SERVERS : {
|
||||
recordOperator = operateServersRecord;
|
||||
break;
|
||||
}
|
||||
case IsolatedEntities.CUSTOM_EMOJI : {
|
||||
recordOperator = operateCustomEmojiRecord;
|
||||
break;
|
||||
}
|
||||
case IsolatedEntities.ROLE : {
|
||||
recordOperator = operateRoleRecord;
|
||||
break;
|
||||
}
|
||||
case IsolatedEntities.SYSTEM : {
|
||||
recordOperator = operateSystemRecord;
|
||||
break;
|
||||
}
|
||||
case IsolatedEntities.TERMS_OF_SERVICE : {
|
||||
recordOperator = operateTermsOfServiceRecord;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
recordOperator = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (recordOperator) {
|
||||
await this.handleBaseData({optType, values, tableName, recordOperator});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* batchOperations: Accepts an instance of Database (either Default or Server) and an array of
|
||||
* prepareCreate/prepareUpdate values and executes the actions on the database.
|
||||
* @param {BatchOperations} operation
|
||||
* @param {Database} operation.db
|
||||
* @param {Array} operation.models
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private batchOperations = async ({db, models}: BatchOperations) => {
|
||||
if (models.length > 0) {
|
||||
await db.action(async () => {
|
||||
await db.batch(...models);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* handleBaseData: Handles the Create/Update operations on an entity.
|
||||
* @param {HandleBaseData} opsBase
|
||||
* @param {OperationType} opsBase.optType
|
||||
* @param {string} opsBase.tableName
|
||||
* @param {Records} opsBase.values
|
||||
* @param {(recordOperator: DataFactory) => void} opsBase.recordOperator
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private handleBaseData = async ({optType, tableName, values, recordOperator}: HandleBaseData) => {
|
||||
const db = await this.getDatabase(tableName);
|
||||
if (!db) {
|
||||
return;
|
||||
}
|
||||
|
||||
let results;
|
||||
const config = {db, optType, tableName};
|
||||
|
||||
if (Array.isArray(values) && values.length) {
|
||||
const recordPromises = await values.map(async (value) => {
|
||||
const record = await recordOperator({...config, value});
|
||||
return record;
|
||||
});
|
||||
|
||||
results = await Promise.all(recordPromises);
|
||||
} else {
|
||||
results = await recordOperator({...config, value: values as RecordValue});
|
||||
}
|
||||
|
||||
if (results) {
|
||||
await this.batchOperations({db, models: Array.isArray(results) ? results : Array(results)});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* getDatabase: Based on the table's name, it will return a database instance either from the 'DEFAULT' database or
|
||||
* the 'SERVER' database.
|
||||
* @param {string} tableName
|
||||
* @returns {Promise<DBInstance>}
|
||||
*/
|
||||
private getDatabase = async (tableName: string): Promise<DBInstance> => {
|
||||
const isInDefaultDB = Object.values(MM_TABLES.DEFAULT).some((tbName) => {
|
||||
return tableName === tbName;
|
||||
});
|
||||
|
||||
if (isInDefaultDB) {
|
||||
return this.defaultDatabase || this.getDefaultDatabase();
|
||||
}
|
||||
|
||||
return this.serverDatabase || this.getServerDatabase();
|
||||
};
|
||||
|
||||
/**
|
||||
* getDefaultDatabase: Returns the default database
|
||||
* @returns {Promise<DBInstance>}
|
||||
*/
|
||||
private getDefaultDatabase = async () => {
|
||||
this.defaultDatabase = await DatabaseManager.getDefaultDatabase();
|
||||
return this.defaultDatabase;
|
||||
};
|
||||
|
||||
/**
|
||||
* getServerDatabase: Returns the current active server database (multi-server support)
|
||||
* @returns {Promise<DBInstance>}
|
||||
*/
|
||||
private getServerDatabase = async () => {
|
||||
this.serverDatabase = await DatabaseManager.getActiveServerDatabase();
|
||||
return this.serverDatabase;
|
||||
};
|
||||
}
|
||||
import DataOperator from '@database/admin/data_operator/handlers';
|
||||
|
||||
export default new DataOperator();
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
import App from '@typings/database/app';
|
||||
import CustomEmoji from '@typings/database/custom_emoji';
|
||||
import {
|
||||
DataFactory,
|
||||
RawApp,
|
||||
RawCustomEmoji,
|
||||
RawGlobal,
|
||||
RawRole,
|
||||
RawServers,
|
||||
RawSystem,
|
||||
RawTermsOfService,
|
||||
} from '@typings/database/database';
|
||||
import Global from '@typings/database/global';
|
||||
import Role from '@typings/database/role';
|
||||
import Servers from '@typings/database/servers';
|
||||
import System from '@typings/database/system';
|
||||
import TermsOfService from '@typings/database/terms_of_service';
|
||||
|
||||
import {OperationType} from './index';
|
||||
|
||||
const {APP, GLOBAL, SERVERS} = MM_TABLES.DEFAULT;
|
||||
const {CUSTOM_EMOJI, ROLE, SYSTEM, TERMS_OF_SERVICE} = MM_TABLES.SERVER;
|
||||
|
||||
/**
|
||||
* operateAppRecord: Prepares record of entity 'App' from the DEFAULT database for update or create actions.
|
||||
* @param {DataFactory} operator
|
||||
* @param {Database} operator.db
|
||||
* @param {OperationType} operator.optType
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const operateAppRecord = async ({db, optType, value}: DataFactory) => {
|
||||
const record = value as RawApp;
|
||||
|
||||
const generator = (app: App) => {
|
||||
app._raw.id = record?.id ?? app.id;
|
||||
app.buildNumber = record?.buildNumber ?? '';
|
||||
app.createdAt = record?.createdAt ?? 0;
|
||||
app.versionNumber = record?.versionNumber ?? '';
|
||||
};
|
||||
|
||||
return operateBaseRecord({db, optType, tableName: APP, value, generator});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateGlobalRecord: Prepares record of entity 'Global' from the DEFAULT database for update or create actions.
|
||||
* @param {DataFactory} operator
|
||||
* @param {Database} operator.db
|
||||
* @param {OperationType} operator.optType
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const operateGlobalRecord = async ({db, optType, value}: DataFactory) => {
|
||||
const record = value as RawGlobal;
|
||||
|
||||
const generator = (global: Global) => {
|
||||
global._raw.id = record?.id ?? global.id;
|
||||
global.name = record?.name ?? '';
|
||||
global.value = record?.value ?? 0;
|
||||
};
|
||||
|
||||
return operateBaseRecord({db, optType, tableName: GLOBAL, value, generator});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateServersRecord: Prepares record of entity 'Servers' from the DEFAULT database for update or create actions.
|
||||
* @param {DataFactory} operator
|
||||
* @param {Database} operator.db
|
||||
* @param {OperationType} operator.optType
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const operateServersRecord = async ({db, optType, value}: DataFactory) => {
|
||||
const record = value as RawServers;
|
||||
|
||||
const generator = (servers: Servers) => {
|
||||
servers._raw.id = record?.id ?? servers.id;
|
||||
servers.dbPath = record?.dbPath ?? '';
|
||||
servers.displayName = record?.displayName ?? 0;
|
||||
servers.mentionCount = record?.mentionCount ?? 0;
|
||||
servers.unreadCount = record?.unreadCount ?? 0;
|
||||
servers.url = record?.url ?? 0;
|
||||
};
|
||||
|
||||
return operateBaseRecord({db, optType, tableName: SERVERS, value, generator});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateCustomEmojiRecord: Prepares record of entity 'CustomEmoji' from the SERVER database for update or create actions.
|
||||
* @param {DataFactory} operator
|
||||
* @param {Database} operator.db
|
||||
* @param {OperationType} operator.optType
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const operateCustomEmojiRecord = async ({db, optType, value}: DataFactory) => {
|
||||
const record = value as RawCustomEmoji;
|
||||
|
||||
const generator = (emoji: CustomEmoji) => {
|
||||
emoji._raw.id = record?.id ?? emoji.id;
|
||||
emoji.name = record?.name ?? '';
|
||||
};
|
||||
|
||||
return operateBaseRecord({db, optType, tableName: CUSTOM_EMOJI, value, generator});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateRoleRecord: Prepares record of entity 'Role' from the SERVER database for update or create actions.
|
||||
* @param {DataFactory} operator
|
||||
* @param {Database} operator.db
|
||||
* @param {OperationType} operator.optType
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const operateRoleRecord = async ({db, optType, value}: DataFactory) => {
|
||||
const record = value as RawRole;
|
||||
|
||||
const generator = (role: Role) => {
|
||||
role._raw.id = record?.id ?? role.id;
|
||||
role.name = record?.name ?? '';
|
||||
role.permissions = record?.permissions ?? [];
|
||||
};
|
||||
|
||||
return operateBaseRecord({db, optType, tableName: ROLE, value, generator});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateSystemRecord: Prepares record of entity 'System' from the SERVER database for update or create actions.
|
||||
* @param {DataFactory} operator
|
||||
* @param {Database} operator.db
|
||||
* @param {OperationType} operator.optType
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const operateSystemRecord = async ({db, optType, value}: DataFactory) => {
|
||||
const record = value as RawSystem;
|
||||
|
||||
const generator = (system: System) => {
|
||||
system._raw.id = record?.id ?? system.id;
|
||||
system.name = record?.name ?? '';
|
||||
system.value = record?.value ?? '';
|
||||
};
|
||||
|
||||
return operateBaseRecord({db, optType, tableName: SYSTEM, value, generator});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateTermsOfServiceRecord: Prepares record of entity 'TermsOfService' from the SERVER database for update or create actions.
|
||||
* @param {DataFactory} operator
|
||||
* @param {Database} operator.db
|
||||
* @param {OperationType} operator.optType
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const operateTermsOfServiceRecord = async ({db, optType, value}: DataFactory) => {
|
||||
const record = value as RawTermsOfService;
|
||||
|
||||
const generator = (tos: TermsOfService) => {
|
||||
tos._raw.id = record?.id ?? tos.id;
|
||||
tos.acceptedAt = record?.acceptedAt ?? 0;
|
||||
};
|
||||
|
||||
return operateBaseRecord({db, optType, tableName: TERMS_OF_SERVICE, value, generator});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateBaseRecord: The 'id' of a record is key to this function. Please note that - at the moment - if WatermelonDB
|
||||
* encounters an existing record during a CREATE operation, it silently fails the operation.
|
||||
*
|
||||
* In our case, we check to see if we have an existing 'id' and if so, we'll update the record with the data.
|
||||
* For an UPDATE operation, we fetch the existing record using the 'id' value and then we do the update operation;
|
||||
* if no record is found for that 'id', we'll create it a new record.
|
||||
*
|
||||
* @param {DataFactory} operatorBase
|
||||
* @param {Database} operatorBase.db
|
||||
* @param {OperationType} operatorBase.optType
|
||||
* @param {string} operatorBase.tableName
|
||||
* @param {any} operatorBase.value
|
||||
* @param {((model: Model) => void)} operatorBase.generator
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const operateBaseRecord = async ({db, optType, tableName, value, generator}: DataFactory) => {
|
||||
// We query first to see if we have a record on that entity with the current value.id
|
||||
const appRecord = await db.collections.get(tableName!).query(Q.where('id', value.id)).fetch() as Model[];
|
||||
const isPresent = appRecord.length > 0;
|
||||
|
||||
if ((isPresent && optType === OperationType.CREATE) || (isPresent && optType === OperationType.UPDATE)) {
|
||||
// Two possible scenarios:
|
||||
// 1. We are dealing with either duplicates here and if so, we'll update instead of create
|
||||
// 2. This is just a normal update operation
|
||||
const record = appRecord[0];
|
||||
return record.prepareUpdate(() => generator!(record));
|
||||
}
|
||||
|
||||
if ((!isPresent && optType === OperationType.UPDATE) || (optType === OperationType.CREATE)) {
|
||||
// Two possible scenarios
|
||||
// 1. We don't have a record yet to update; so we create it
|
||||
// 2. This is just a normal create operation
|
||||
return db.collections.get(tableName!).prepareCreate(generator);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
507
app/database/admin/data_operator/operators/index.ts
Normal file
507
app/database/admin/data_operator/operators/index.ts
Normal file
@@ -0,0 +1,507 @@
|
||||
// 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 App from '@typings/database/app';
|
||||
import CustomEmoji from '@typings/database/custom_emoji';
|
||||
import {
|
||||
BaseOperator,
|
||||
IdenticalRecord,
|
||||
Operator,
|
||||
RawApp,
|
||||
RawCustomEmoji,
|
||||
RawDraft,
|
||||
RawFile,
|
||||
RawGlobal,
|
||||
RawPost,
|
||||
RawPostMetadata,
|
||||
RawPostsInChannel,
|
||||
RawPostsInThread,
|
||||
RawReaction,
|
||||
RawRole,
|
||||
RawServers,
|
||||
RawSystem,
|
||||
RawTermsOfService,
|
||||
} from '@typings/database/database';
|
||||
import Draft from '@typings/database/draft';
|
||||
import File from '@typings/database/file';
|
||||
import Global from '@typings/database/global';
|
||||
import Post from '@typings/database/post';
|
||||
import PostMetadata from '@typings/database/post_metadata';
|
||||
import PostsInChannel from '@typings/database/posts_in_channel';
|
||||
import PostsInThread from '@typings/database/posts_in_thread';
|
||||
import Reaction from '@typings/database/reaction';
|
||||
import Role from '@typings/database/role';
|
||||
import Servers from '@typings/database/servers';
|
||||
import System from '@typings/database/system';
|
||||
import TermsOfService from '@typings/database/terms_of_service';
|
||||
|
||||
const {APP, GLOBAL, SERVERS} = MM_TABLES.DEFAULT;
|
||||
const {
|
||||
CUSTOM_EMOJI,
|
||||
DRAFT,
|
||||
FILE,
|
||||
POST,
|
||||
POST_METADATA,
|
||||
POSTS_IN_CHANNEL,
|
||||
POSTS_IN_THREAD,
|
||||
REACTION,
|
||||
ROLE,
|
||||
SYSTEM,
|
||||
TERMS_OF_SERVICE,
|
||||
} = MM_TABLES.SERVER;
|
||||
|
||||
/**
|
||||
* operateAppRecord: Prepares record of entity 'App' from the DEFAULT database for update or create actions.
|
||||
* @param {Operator} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<Model>}
|
||||
*/
|
||||
export const operateAppRecord = async ({database, value}: Operator) => {
|
||||
const record = value as RawApp;
|
||||
|
||||
const generator = (app: App) => {
|
||||
app._raw.id = record?.id ?? app.id;
|
||||
app.buildNumber = record?.buildNumber;
|
||||
app.createdAt = record?.createdAt;
|
||||
app.versionNumber = record?.versionNumber;
|
||||
};
|
||||
|
||||
return operateBaseRecord({
|
||||
database,
|
||||
tableName: APP,
|
||||
value,
|
||||
generator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateGlobalRecord: Prepares record of entity 'Global' from the DEFAULT database for update or create actions.
|
||||
* @param {Operator} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<Model>}
|
||||
*/
|
||||
export const operateGlobalRecord = async ({database, value}: Operator) => {
|
||||
const record = value as RawGlobal;
|
||||
|
||||
const generator = (global: Global) => {
|
||||
global._raw.id = record?.id ?? global.id;
|
||||
global.name = record?.name;
|
||||
global.value = record?.value;
|
||||
};
|
||||
|
||||
return operateBaseRecord({
|
||||
database,
|
||||
tableName: GLOBAL,
|
||||
value,
|
||||
generator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateServersRecord: Prepares record of entity 'Servers' from the DEFAULT database for update or create actions.
|
||||
* @param {Operator} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<Model>}
|
||||
*/
|
||||
export const operateServersRecord = async ({database, value}: Operator) => {
|
||||
const record = value as RawServers;
|
||||
|
||||
const generator = (servers: Servers) => {
|
||||
servers._raw.id = record?.id ?? servers.id;
|
||||
servers.dbPath = record?.dbPath;
|
||||
servers.displayName = record?.displayName;
|
||||
servers.mentionCount = record?.mentionCount;
|
||||
servers.unreadCount = record?.unreadCount;
|
||||
servers.url = record?.url;
|
||||
};
|
||||
|
||||
return operateBaseRecord({
|
||||
database,
|
||||
tableName: SERVERS,
|
||||
value,
|
||||
generator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateCustomEmojiRecord: Prepares record of entity 'CustomEmoji' from the SERVER database for update or create actions.
|
||||
* @param {Operator} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<Model>}
|
||||
*/
|
||||
export const operateCustomEmojiRecord = async ({database, value}: Operator) => {
|
||||
const record = value as RawCustomEmoji;
|
||||
const generator = (emoji: CustomEmoji) => {
|
||||
emoji._raw.id = record?.id ?? emoji.id;
|
||||
emoji.name = record.name;
|
||||
};
|
||||
|
||||
const appRecord = (await database.collections.
|
||||
get(CUSTOM_EMOJI!).
|
||||
query(Q.where('name', record.name)).
|
||||
fetch()) as Model[];
|
||||
const isPresent = appRecord.length > 0;
|
||||
|
||||
if (isPresent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return operateBaseRecord({
|
||||
database,
|
||||
tableName: CUSTOM_EMOJI,
|
||||
value,
|
||||
generator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateRoleRecord: Prepares record of entity 'Role' from the SERVER database for update or create actions.
|
||||
* @param {Operator} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<Model>}
|
||||
*/
|
||||
export const operateRoleRecord = async ({database, value}: Operator) => {
|
||||
const record = value as RawRole;
|
||||
|
||||
const generator = (role: Role) => {
|
||||
role._raw.id = record?.id ?? role.id;
|
||||
role.name = record?.name;
|
||||
role.permissions = record?.permissions;
|
||||
};
|
||||
|
||||
return operateBaseRecord({
|
||||
database,
|
||||
tableName: ROLE,
|
||||
value,
|
||||
generator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateSystemRecord: Prepares record of entity 'System' from the SERVER database for update or create actions.
|
||||
* @param {Operator} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<Model>}
|
||||
*/
|
||||
export const operateSystemRecord = async ({database, value}: Operator) => {
|
||||
const record = value as RawSystem;
|
||||
|
||||
const generator = (system: System) => {
|
||||
system._raw.id = record?.id ?? system.id;
|
||||
system.name = record?.name;
|
||||
system.value = record?.value;
|
||||
};
|
||||
|
||||
return operateBaseRecord({
|
||||
database,
|
||||
tableName: SYSTEM,
|
||||
value,
|
||||
generator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateTermsOfServiceRecord: Prepares record of entity 'TermsOfService' from the SERVER database for update or create actions.
|
||||
* @param {Operator} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<Model>}
|
||||
*/
|
||||
export const operateTermsOfServiceRecord = async ({database, value}: Operator) => {
|
||||
const record = value as RawTermsOfService;
|
||||
|
||||
const generator = (tos: TermsOfService) => {
|
||||
tos._raw.id = record?.id ?? tos.id;
|
||||
tos.acceptedAt = record?.acceptedAt;
|
||||
};
|
||||
|
||||
return operateBaseRecord({
|
||||
database,
|
||||
tableName: TERMS_OF_SERVICE,
|
||||
value,
|
||||
generator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* operatePostRecord: Prepares record of entity 'Post' from the SERVER database for update or create actions.
|
||||
* @param {Operator} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<Model>}
|
||||
*/
|
||||
export const operatePostRecord = async ({database, value}: Operator) => {
|
||||
const record = value as RawPost;
|
||||
|
||||
const generator = (post: Post) => {
|
||||
post._raw.id = record?.id;
|
||||
post.channelId = record?.channel_id;
|
||||
post.createAt = record?.create_at;
|
||||
post.deleteAt = record?.delete_at || record?.delete_at === 0 ? record?.delete_at : 0;
|
||||
post.editAt = record?.edit_at;
|
||||
post.updateAt = record?.update_at;
|
||||
post.isPinned = record!.is_pinned!;
|
||||
post.message = Q.sanitizeLikeString(record?.message);
|
||||
post.userId = record?.user_id;
|
||||
post.originalId = record?.original_id ?? '';
|
||||
post.pendingPostId = record?.pending_post_id ?? '';
|
||||
post.previousPostId = record?.prev_post_id ?? '';
|
||||
post.rootId = record?.root_id ?? '';
|
||||
post.type = record?.type ?? '';
|
||||
post.props = record?.props ?? {};
|
||||
};
|
||||
|
||||
return operateBaseRecord({
|
||||
database,
|
||||
tableName: POST,
|
||||
value,
|
||||
generator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* operatePostInThreadRecord: Prepares record of entity 'POSTS_IN_THREAD' from the SERVER database for update or create actions.
|
||||
* @param {Operator} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<Model>}
|
||||
*/
|
||||
export const operatePostInThreadRecord = async ({database, value}: Operator) => {
|
||||
const record = value as RawPostsInThread;
|
||||
|
||||
const generator = (postsInThread: PostsInThread) => {
|
||||
postsInThread._raw.id = postsInThread.id;
|
||||
postsInThread.postId = record.post_id;
|
||||
postsInThread.earliest = record.earliest;
|
||||
postsInThread.latest = record.latest!;
|
||||
};
|
||||
|
||||
return operateBaseRecord({
|
||||
database,
|
||||
tableName: POSTS_IN_THREAD,
|
||||
value,
|
||||
generator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateReactionRecord: Prepares record of entity 'REACTION' from the SERVER database for update or create actions.
|
||||
* @param {Operator} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<Model>}
|
||||
*/
|
||||
export const operateReactionRecord = async ({database, value}: Operator) => {
|
||||
const record = value as RawReaction;
|
||||
|
||||
const generator = (reaction: Reaction) => {
|
||||
reaction._raw.id = reaction.id;
|
||||
reaction.userId = record.user_id;
|
||||
reaction.postId = record.post_id;
|
||||
reaction.emojiName = record.emoji_name;
|
||||
reaction.createAt = record.create_at;
|
||||
};
|
||||
|
||||
return operateBaseRecord({
|
||||
database,
|
||||
tableName: REACTION,
|
||||
value,
|
||||
generator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateFileRecord: Prepares record of entity 'FILE' from the SERVER database for update or create actions.
|
||||
* @param {Operator} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<Model>}
|
||||
*/
|
||||
export const operateFileRecord = async ({database, value}: Operator) => {
|
||||
const record = value as RawFile;
|
||||
|
||||
const generator = (file: File) => {
|
||||
file._raw.id = record?.id ?? file.id;
|
||||
file.postId = record.post_id;
|
||||
file.name = record.name;
|
||||
file.extension = record.extension;
|
||||
file.size = record.size;
|
||||
file.mimeType = record?.mime_type ?? '';
|
||||
file.width = record?.width ?? 0;
|
||||
file.height = record?.height ?? 0;
|
||||
file.imageThumbnail = record?.mini_preview ?? '';
|
||||
file.localPath = record?.localPath ?? '';
|
||||
};
|
||||
|
||||
return operateBaseRecord({
|
||||
database,
|
||||
tableName: FILE,
|
||||
value,
|
||||
generator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* operatePostMetadataRecord: Prepares record of entity 'POST_METADATA' from the SERVER database for update or create actions.
|
||||
* @param {Operator} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<Model>}
|
||||
*/
|
||||
export const operatePostMetadataRecord = async ({database, value}: Operator) => {
|
||||
const record = value as RawPostMetadata;
|
||||
|
||||
const generator = (postMeta: PostMetadata) => {
|
||||
postMeta._raw.id = postMeta.id;
|
||||
postMeta.data = record.data;
|
||||
postMeta.postId = record.postId;
|
||||
postMeta.type = record.type;
|
||||
};
|
||||
|
||||
return operateBaseRecord({
|
||||
database,
|
||||
tableName: POST_METADATA,
|
||||
value,
|
||||
generator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateDraftRecord: Prepares record of entity 'DRAFT' from the SERVER database for update or create actions.
|
||||
* @param {Operator} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<Model>}
|
||||
*/
|
||||
export const operateDraftRecord = async ({database, value}: Operator) => {
|
||||
const record = value as RawDraft;
|
||||
const emptyFileInfo: FileInfo[] = [];
|
||||
|
||||
const generator = (draft: Draft) => {
|
||||
draft._raw.id = record?.id ?? draft.id;
|
||||
draft.rootId = record?.root_id ?? '';
|
||||
draft.message = record?.message ?? '';
|
||||
draft.channelId = record?.channel_id ?? '';
|
||||
draft.files = record?.files ?? emptyFileInfo;
|
||||
};
|
||||
|
||||
return operateBaseRecord({
|
||||
database,
|
||||
tableName: DRAFT,
|
||||
value,
|
||||
generator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* operatePostsInChannelRecord: Prepares record of entity 'POSTS_IN_CHANNEL' from the SERVER database for update or create actions.
|
||||
* @param {Operator} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordValue} operator.value
|
||||
* @returns {Promise<Model>}
|
||||
*/
|
||||
export const operatePostsInChannelRecord = async ({database, value}: Operator) => {
|
||||
const record = value as RawPostsInChannel;
|
||||
|
||||
const generator = (postsInChannel: PostsInChannel) => {
|
||||
postsInChannel._raw.id = record?.id ?? postsInChannel.id;
|
||||
postsInChannel.channelId = record.channel_id;
|
||||
postsInChannel.earliest = record.earliest;
|
||||
postsInChannel.latest = record.latest;
|
||||
};
|
||||
|
||||
return operateBaseRecord({
|
||||
database,
|
||||
tableName: POSTS_IN_CHANNEL,
|
||||
value,
|
||||
generator,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* operateBaseRecord: The 'id' of a record is key to this function. Please note that - at the moment - if WatermelonDB
|
||||
* encounters an existing record during a CREATE operation, it silently fails the operation.
|
||||
*
|
||||
* This operator decides to go through an UPDATE action if we have an existing record in the table bearing the same id.
|
||||
* If not, it will go for a CREATE operation.
|
||||
*
|
||||
* However, if the tableName points to a major entity ( like Post, User or Channel, etc.), it verifies first if the
|
||||
* update_at value of the existing record is different from the parameter 'value' own update_at. Only if they differ,
|
||||
* that it prepares the record for update.
|
||||
*
|
||||
* @param {Operator} operatorBase
|
||||
* @param {Database} operatorBase.database
|
||||
* @param {string} operatorBase.tableName
|
||||
* @param {RecordValue} operatorBase.value
|
||||
* @param {((model: Model) => void)} operatorBase.generator
|
||||
* @returns {Promise<Model>}
|
||||
*/
|
||||
const operateBaseRecord = async ({database, tableName, value, generator}: BaseOperator) => {
|
||||
// We query first to see if we have a record on that entity with the current value.id
|
||||
const appRecord = (await database.collections.
|
||||
get(tableName!).
|
||||
query(Q.where('id', value.id!)).
|
||||
fetch()) as Model[];
|
||||
|
||||
const isPresent = appRecord.length > 0;
|
||||
|
||||
if (isPresent) {
|
||||
const record = appRecord[0];
|
||||
|
||||
// We avoid unnecessary updates if we already have a record with the same update_at value for this model/entity
|
||||
const isRecordIdentical = checkForIdenticalRecord({
|
||||
tableName: tableName!,
|
||||
newValue: value,
|
||||
existingRecord: record,
|
||||
});
|
||||
|
||||
if (isRecordIdentical) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Two possible scenarios:
|
||||
// 1. We are dealing with either duplicates here and if so, we'll update instead of create
|
||||
// 2. This is just a normal update operation
|
||||
return record.prepareUpdate(() => generator!(record));
|
||||
}
|
||||
|
||||
// Two possible scenarios
|
||||
// 1. We don't have a record yet to update; so we create it
|
||||
// 2. This is just a normal create operation
|
||||
return database.collections.get(tableName!).prepareCreate(generator);
|
||||
};
|
||||
|
||||
/**
|
||||
* checkForIdenticalRecord:
|
||||
* @param {IdenticalRecord} identicalRecord
|
||||
* @param {string} identicalRecord.tableName
|
||||
* @param {RecordValue} identicalRecord.newValue
|
||||
* @param {Model} identicalRecord.existingRecord
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const checkForIdenticalRecord = ({tableName, newValue, existingRecord}: IdenticalRecord) => {
|
||||
const guardTables = [POST];
|
||||
if (guardTables.includes(tableName)) {
|
||||
switch (tableName) {
|
||||
case POST: {
|
||||
const tempPost = newValue as RawPost;
|
||||
const currentRecord = (existingRecord as unknown) as Post;
|
||||
return tempPost.update_at === currentRecord.updateAt;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
544
app/database/admin/data_operator/operators/test.ts
Normal file
544
app/database/admin/data_operator/operators/test.ts
Normal file
@@ -0,0 +1,544 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import DatabaseManager from '@database/admin/database_manager';
|
||||
import DataOperator from '@database/admin/data_operator';
|
||||
import App from '@typings/database/app';
|
||||
import {DatabaseType, IsolatedEntities} from '@typings/database/enums';
|
||||
|
||||
import {
|
||||
operateAppRecord,
|
||||
operateCustomEmojiRecord,
|
||||
operateDraftRecord,
|
||||
operateFileRecord,
|
||||
operateGlobalRecord,
|
||||
operatePostInThreadRecord,
|
||||
operatePostMetadataRecord,
|
||||
operatePostRecord,
|
||||
operatePostsInChannelRecord,
|
||||
operateReactionRecord,
|
||||
operateRoleRecord,
|
||||
operateServersRecord,
|
||||
operateSystemRecord,
|
||||
operateTermsOfServiceRecord,
|
||||
} from './index';
|
||||
|
||||
jest.mock('@database/admin/database_manager');
|
||||
|
||||
const {APP} = MM_TABLES.DEFAULT;
|
||||
|
||||
describe('*** DataOperator: Operators tests ***', () => {
|
||||
const createConnection = async (setActive = false) => {
|
||||
const dbName = 'server_schema_connection';
|
||||
const serverUrl = 'https://appv2.mattermost.com';
|
||||
const database = await DatabaseManager.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl,
|
||||
},
|
||||
});
|
||||
|
||||
if (setActive) {
|
||||
await DatabaseManager.setActiveServerDatabase({
|
||||
displayName: dbName,
|
||||
serverUrl,
|
||||
});
|
||||
}
|
||||
|
||||
return database;
|
||||
};
|
||||
|
||||
it('=> operateAppRecord: should return an array of type App', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await DatabaseManager.getDefaultDatabase();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateAppRecord({
|
||||
database: database!,
|
||||
value: {
|
||||
buildNumber: 'build-7',
|
||||
createdAt: 1,
|
||||
id: 'id-18',
|
||||
versionNumber: 'v-1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('App');
|
||||
});
|
||||
|
||||
it('=> operateGlobalRecord: should return an array of type Global', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await DatabaseManager.getDefaultDatabase();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateGlobalRecord({
|
||||
database: database!,
|
||||
value: {id: 'g-1', name: 'g-n1', value: 'g-v1'},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('Global');
|
||||
});
|
||||
|
||||
it('=> operateServersRecord: should return an array of type Servers', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await DatabaseManager.getDefaultDatabase();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateServersRecord({
|
||||
database: database!,
|
||||
value: {
|
||||
dbPath: 'mm-server',
|
||||
displayName: 's-displayName',
|
||||
id: 's-1',
|
||||
mentionCount: 1,
|
||||
unreadCount: 0,
|
||||
url: 'https://community.mattermost.com',
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('Servers');
|
||||
});
|
||||
|
||||
it('=> operateCustomEmojiRecord: should return an array of type CustomEmoji', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createConnection();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateCustomEmojiRecord({
|
||||
database: database!,
|
||||
value: {id: 'emo-1', name: 'emoji'},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('CustomEmoji');
|
||||
});
|
||||
|
||||
it('=> operateRoleRecord: should return an array of type Role', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createConnection();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateRoleRecord({
|
||||
database: database!,
|
||||
value: {id: 'role-1', name: 'role-name-1', permissions: []},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('Role');
|
||||
});
|
||||
|
||||
it('=> operateSystemRecord: should return an array of type System', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createConnection();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateSystemRecord({
|
||||
database: database!,
|
||||
value: {id: 'system-1', name: 'system-name-1', value: 'system'},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('System');
|
||||
});
|
||||
|
||||
it('=> operateTermsOfServiceRecord: should return an array of type TermsOfService', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createConnection();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateTermsOfServiceRecord({
|
||||
database: database!,
|
||||
value: {id: 'system-1', acceptedAt: 1},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch(
|
||||
'TermsOfService',
|
||||
);
|
||||
});
|
||||
|
||||
it('=> should create a record in the App table in the default database', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
// Creates a record in the App table
|
||||
await DataOperator.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.APP,
|
||||
values: [
|
||||
{
|
||||
buildNumber: 'build-1',
|
||||
createdAt: 1,
|
||||
id: 'id-1',
|
||||
versionNumber: 'version-1',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Do a query and find out if the value has been registered in the App table of the default database
|
||||
const connection = await DatabaseManager.getDefaultDatabase();
|
||||
expect(connection).toBeTruthy();
|
||||
|
||||
const records = (await connection!.collections.
|
||||
get(APP).
|
||||
query(Q.where('id', 'id-1')).
|
||||
fetch()) as App[];
|
||||
|
||||
// We should expect to have a record returned as dictated by our query
|
||||
expect(records.length).toBe(1);
|
||||
});
|
||||
|
||||
it('=> should create several records in the App table in the default database', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
// Creates a record in the App table
|
||||
await DataOperator.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.APP,
|
||||
values: [
|
||||
{
|
||||
buildNumber: 'build-10',
|
||||
createdAt: 1,
|
||||
id: 'id-10',
|
||||
versionNumber: 'version-10',
|
||||
},
|
||||
{
|
||||
buildNumber: 'build-11',
|
||||
createdAt: 1,
|
||||
id: 'id-11',
|
||||
versionNumber: 'version-11',
|
||||
},
|
||||
{
|
||||
buildNumber: 'build-12',
|
||||
createdAt: 1,
|
||||
id: 'id-12',
|
||||
versionNumber: 'version-12',
|
||||
},
|
||||
{
|
||||
buildNumber: 'build-13',
|
||||
createdAt: 1,
|
||||
id: 'id-13',
|
||||
versionNumber: 'version-13',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Do a query and find out if the value has been registered in the App table of the default database
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
const records = (await defaultDB!.collections.
|
||||
get(APP).
|
||||
query(Q.where('id', Q.oneOf(['id-10', 'id-11', 'id-12', 'id-13']))).
|
||||
fetch()) as App[];
|
||||
|
||||
// We should expect to have 4 records created
|
||||
expect(records.length).toBe(4);
|
||||
});
|
||||
|
||||
it('=> should update a record in the App table in the default database', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
// Update record having id 'id-1'
|
||||
await DataOperator.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.APP,
|
||||
values: [
|
||||
{
|
||||
buildNumber: 'build-13-13',
|
||||
createdAt: 1,
|
||||
id: 'id-1',
|
||||
versionNumber: 'version-1',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const records = (await defaultDB!.collections.
|
||||
get(APP).
|
||||
query(Q.where('id', 'id-1')).
|
||||
fetch()) as App[];
|
||||
expect(records.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify if the buildNumber for this record has been updated
|
||||
expect(records[0].buildNumber).toMatch('build-13-13');
|
||||
});
|
||||
|
||||
it('=> should update several records in the App table in the default database', async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
// Update records having id 'id-10' and 'id-11'
|
||||
await DataOperator.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.APP,
|
||||
values: [
|
||||
{
|
||||
buildNumber: 'build-10x',
|
||||
createdAt: 1,
|
||||
id: 'id-10',
|
||||
versionNumber: 'version-10',
|
||||
},
|
||||
{
|
||||
buildNumber: 'build-11y',
|
||||
createdAt: 1,
|
||||
id: 'id-11',
|
||||
versionNumber: 'version-11',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const records = (await defaultDB!.collections.
|
||||
get(APP).
|
||||
query(Q.where('id', Q.oneOf(['id-10', 'id-11']))).
|
||||
fetch()) as App[];
|
||||
expect(records.length).toBe(2);
|
||||
|
||||
// Verify if the buildNumber for those two record has been updated
|
||||
expect(records[0].buildNumber).toMatch('build-10x');
|
||||
expect(records[1].buildNumber).toMatch('build-11y');
|
||||
});
|
||||
|
||||
it('=> [EDGE CASE] should UPDATE instead of CREATE record for existing id', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
await DataOperator.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.APP,
|
||||
values: [
|
||||
{
|
||||
buildNumber: 'build-10x',
|
||||
createdAt: 1,
|
||||
id: 'id-10',
|
||||
versionNumber: 'version-10',
|
||||
},
|
||||
{
|
||||
buildNumber: 'build-11x',
|
||||
createdAt: 1,
|
||||
id: 'id-11',
|
||||
versionNumber: 'version-11',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const records = (await defaultDB!.collections.
|
||||
get(APP).
|
||||
query(Q.where('id', Q.oneOf(['id-10', 'id-11']))).
|
||||
fetch()) as App[];
|
||||
|
||||
// Verify if the buildNumber for those two record has been updated
|
||||
expect(records[0].buildNumber).toMatch('build-10x');
|
||||
expect(records[1].buildNumber).toMatch('build-11x');
|
||||
});
|
||||
|
||||
it('=> [EDGE CASE] should CREATE instead of UPDATE record for non-existing id', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
// id-15 and id-16 do not exist but yet the optType is UPDATE. The operator should then prepareCreate the records instead of prepareUpdate
|
||||
await DataOperator.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.APP,
|
||||
values: [
|
||||
{
|
||||
buildNumber: 'build-10x',
|
||||
createdAt: 1,
|
||||
id: 'id-15',
|
||||
versionNumber: 'version-10',
|
||||
},
|
||||
{
|
||||
buildNumber: 'build-11x',
|
||||
createdAt: 1,
|
||||
id: 'id-16',
|
||||
versionNumber: 'version-11',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const records = (await defaultDB!.collections.
|
||||
get(APP).
|
||||
query(Q.where('id', Q.oneOf(['id-15', 'id-16']))).
|
||||
fetch()) as App[];
|
||||
|
||||
// Verify if the buildNumber for those two record has been created
|
||||
expect(records[0].buildNumber).toMatch('build-10x');
|
||||
expect(records[1].buildNumber).toMatch('build-11x');
|
||||
});
|
||||
|
||||
it('=> operatePostRecord: should return an array of type Post', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createConnection();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operatePostRecord({
|
||||
database: database!,
|
||||
value: {
|
||||
id: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
create_at: 1596032651748,
|
||||
update_at: 1596032651748,
|
||||
edit_at: 0,
|
||||
delete_at: 0,
|
||||
is_pinned: false,
|
||||
user_id: 'q3mzxua9zjfczqakxdkowc6u6yy',
|
||||
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
|
||||
root_id: 'ps81iqbesfby8jayz7owg4yypoo',
|
||||
parent_id: 'ps81iqbddesfby8jayz7owg4yypoo',
|
||||
original_id: '',
|
||||
message: 'Testing operator post',
|
||||
type: '',
|
||||
props: {},
|
||||
hashtags: '',
|
||||
pending_post_id: '',
|
||||
reply_count: 4,
|
||||
last_reply_at: 0,
|
||||
participants: null,
|
||||
metadata: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('Post');
|
||||
});
|
||||
|
||||
it('=> operatePostInThreadRecord: should return an array of type PostsInThread', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createConnection();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operatePostInThreadRecord({
|
||||
database: database!,
|
||||
value: {
|
||||
id: 'ps81iqbddesfby8jayz7owg4yypoo',
|
||||
post_id: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
earliest: 1596032651748,
|
||||
latest: 1597032651748,
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('PostsInThread');
|
||||
});
|
||||
|
||||
it('=> operateReactionRecord: should return an array of type Reaction', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createConnection();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateReactionRecord({
|
||||
database: database!,
|
||||
value: {
|
||||
id: 'ps81iqbddesfby8jayz7owg4yypoo',
|
||||
user_id: 'q3mzxua9zjfczqakxdkowc6u6yy',
|
||||
post_id: 'ps81iqbddesfby8jayz7owg4yypoo',
|
||||
emoji_name: 'thumbsup',
|
||||
create_at: 1596032651748,
|
||||
update_at: 1608253011321,
|
||||
delete_at: 0,
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('Reaction');
|
||||
});
|
||||
|
||||
it('=> operateFileRecord: should return an array of type File', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createConnection();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateFileRecord({
|
||||
database: database!,
|
||||
value: {
|
||||
post_id: 'ps81iqbddesfby8jayz7owg4yypoo',
|
||||
name: 'test_file',
|
||||
extension: '.jpg',
|
||||
size: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('File');
|
||||
});
|
||||
|
||||
it('=> operatePostMetadataRecord: should return an array of type PostMetadata', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createConnection();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operatePostMetadataRecord({
|
||||
database: database!,
|
||||
value: {
|
||||
id: 'ps81i4yypoo',
|
||||
data: {},
|
||||
postId: 'ps81iqbddesfby8jayz7owg4yypoo',
|
||||
type: 'opengraph',
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('PostMetadata');
|
||||
});
|
||||
|
||||
it('=> operateDraftRecord: should return an array of type Draft', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createConnection();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateDraftRecord({
|
||||
database: database!,
|
||||
value: {
|
||||
id: 'ps81i4yypoo',
|
||||
root_id: 'ps81iqbddesfby8jayz7owg4yypoo',
|
||||
message: 'draft message',
|
||||
channel_id: 'channel_idp23232e',
|
||||
files: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('Draft');
|
||||
});
|
||||
|
||||
it('=> operatePostsInChannelRecord: should return an array of type PostsInChannel', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createConnection();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operatePostsInChannelRecord({
|
||||
database: database!,
|
||||
value: {
|
||||
id: 'ps81i4yypoo',
|
||||
channel_id: 'channel_idp23232e',
|
||||
earliest: 1608253011321,
|
||||
latest: 1609253011321,
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('PostsInChannel');
|
||||
});
|
||||
});
|
||||
@@ -1,481 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import App from '@typings/database/app';
|
||||
|
||||
import DatabaseManager, {DatabaseType} from '../database_manager';
|
||||
import DataOperator, {IsolatedEntities, OperationType} from './index';
|
||||
import {
|
||||
operateAppRecord,
|
||||
operateCustomEmojiRecord,
|
||||
operateGlobalRecord,
|
||||
operateRoleRecord,
|
||||
operateServersRecord,
|
||||
operateSystemRecord,
|
||||
operateTermsOfServiceRecord,
|
||||
} from './operators';
|
||||
|
||||
jest.mock('../database_manager');
|
||||
|
||||
const {APP} = MM_TABLES.DEFAULT;
|
||||
|
||||
describe('*** Data Operator tests ***', () => {
|
||||
it('=> should return an array of type App for operateAppRecord', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const db = await DatabaseManager.getDefaultDatabase();
|
||||
expect(db).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateAppRecord({
|
||||
db: db!,
|
||||
optType: OperationType.CREATE,
|
||||
value: {buildNumber: 'build-7', createdAt: 1, id: 'id-18', versionNumber: 'v-1'},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('App');
|
||||
});
|
||||
|
||||
it('=> should return an array of type Global for operateGlobalRecord', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const db = await DatabaseManager.getDefaultDatabase();
|
||||
expect(db).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateGlobalRecord({
|
||||
db: db!,
|
||||
optType: OperationType.CREATE,
|
||||
value: {id: 'g-1', name: 'g-n1', value: 'g-v1'},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('Global');
|
||||
});
|
||||
|
||||
it('=> should return an array of type Servers for operateServersRecord', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const db = await DatabaseManager.getDefaultDatabase();
|
||||
expect(db).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateServersRecord({
|
||||
db: db!,
|
||||
optType: OperationType.CREATE,
|
||||
value: {
|
||||
dbPath: 'mm-server',
|
||||
displayName: 's-displayName',
|
||||
id: 's-1',
|
||||
mentionCount: 1,
|
||||
unreadCount: 0,
|
||||
url: 'https://community.mattermost.com',
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('Servers');
|
||||
});
|
||||
|
||||
it('=> should return an array of type CustomEmoji for operateCustomEmojiRecord', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const db = await DatabaseManager.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
databaseConnection: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'community mattermost',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: 'https://appv2.mattermost.com',
|
||||
},
|
||||
});
|
||||
expect(db).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateCustomEmojiRecord({
|
||||
db: db!,
|
||||
optType: OperationType.CREATE,
|
||||
value: {id: 'emo-1', name: 'emoji'},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('CustomEmoji');
|
||||
});
|
||||
|
||||
it('=> should return an array of type Role for operateRoleRecord', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const db = await DatabaseManager.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
databaseConnection: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'community mattermost',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: 'https://appv2.mattermost.com',
|
||||
},
|
||||
});
|
||||
expect(db).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateRoleRecord({
|
||||
db: db!,
|
||||
optType: OperationType.CREATE,
|
||||
value: {id: 'role-1', name: 'role-name-1', permissions: []},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('Role');
|
||||
});
|
||||
|
||||
it('=> should return an array of type System for operateSystemRecord', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const db = await DatabaseManager.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
databaseConnection: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'community mattermost',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: 'https://appv2.mattermost.com',
|
||||
},
|
||||
});
|
||||
expect(db).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateSystemRecord({
|
||||
db: db!,
|
||||
optType: OperationType.CREATE,
|
||||
value: {id: 'system-1', name: 'system-name-1', value: 'system'},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('System');
|
||||
});
|
||||
|
||||
it('=> should return an array of type TermsOfService for operateTermsOfServiceRecord', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const db = await DatabaseManager.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
databaseConnection: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'community mattermost',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: 'https://appv2.mattermost.com',
|
||||
},
|
||||
});
|
||||
expect(db).toBeTruthy();
|
||||
|
||||
const preparedRecords = await operateTermsOfServiceRecord({
|
||||
db: db!,
|
||||
optType: OperationType.CREATE,
|
||||
value: {id: 'system-1', acceptedAt: 1},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.modelClass.name).toMatch('TermsOfService');
|
||||
});
|
||||
|
||||
it('=> should create a record in the App table in the default database', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
// Creates a record in the App table
|
||||
await DataOperator.handleIsolatedEntityData({
|
||||
optType: OperationType.CREATE,
|
||||
tableName: IsolatedEntities.APP,
|
||||
values: {buildNumber: 'build-1', createdAt: 1, id: 'id-1', versionNumber: 'version-1'},
|
||||
});
|
||||
|
||||
// Do a query and find out if the value has been registered in the App table of the default database
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
const records = await defaultDB!.collections.get(APP).query(Q.where('id', 'id-1')).fetch() as App[];
|
||||
|
||||
// We should expect to have a record returned as dictated by our query
|
||||
expect(records.length).toBe(1);
|
||||
});
|
||||
|
||||
it('=> should create several records in the App table in the default database', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
// Creates a record in the App table
|
||||
await DataOperator.handleIsolatedEntityData({
|
||||
optType: OperationType.CREATE,
|
||||
tableName: IsolatedEntities.APP,
|
||||
values: [
|
||||
{buildNumber: 'build-10', createdAt: 1, id: 'id-10', versionNumber: 'version-10'},
|
||||
{buildNumber: 'build-11', createdAt: 1, id: 'id-11', versionNumber: 'version-11'},
|
||||
{buildNumber: 'build-12', createdAt: 1, id: 'id-12', versionNumber: 'version-12'},
|
||||
{buildNumber: 'build-13', createdAt: 1, id: 'id-13', versionNumber: 'version-13'},
|
||||
],
|
||||
});
|
||||
|
||||
// Do a query and find out if the value has been registered in the App table of the default database
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
const records = await defaultDB!.collections.get(APP).query(Q.where('id', Q.oneOf(['id-10', 'id-11', 'id-12', 'id-13']))).fetch() as App[];
|
||||
|
||||
// We should expect to have 4 records created
|
||||
expect(records.length).toBe(4);
|
||||
});
|
||||
|
||||
it('=> should update a record in the App table in the default database', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
// Update record having id 'id-1'
|
||||
await DataOperator.handleIsolatedEntityData({
|
||||
optType: OperationType.UPDATE,
|
||||
tableName: IsolatedEntities.APP,
|
||||
values: {buildNumber: 'build-13-13', createdAt: 1, id: 'id-1', versionNumber: 'version-1'},
|
||||
});
|
||||
|
||||
const records = await defaultDB!.collections.get(APP).query(Q.where('id', 'id-1')).fetch() as App[];
|
||||
expect(records.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify if the buildNumber for this record has been updated
|
||||
expect(records[0].buildNumber).toMatch('build-13-13');
|
||||
});
|
||||
|
||||
it('=> should update several records in the App table in the default database', async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
// Update records having id 'id-10' and 'id-11'
|
||||
await DataOperator.handleIsolatedEntityData({
|
||||
optType: OperationType.UPDATE,
|
||||
tableName: IsolatedEntities.APP,
|
||||
values: [
|
||||
{buildNumber: 'build-10x', createdAt: 1, id: 'id-10', versionNumber: 'version-10'},
|
||||
{buildNumber: 'build-11y', createdAt: 1, id: 'id-11', versionNumber: 'version-11'},
|
||||
],
|
||||
});
|
||||
|
||||
const records = await defaultDB!.collections.get(APP).query(Q.where('id', Q.oneOf(['id-10', 'id-11']))).fetch() as App[];
|
||||
expect(records.length).toBe(2);
|
||||
|
||||
// Verify if the buildNumber for those two record has been updated
|
||||
expect(records[0].buildNumber).toMatch('build-10x');
|
||||
expect(records[1].buildNumber).toMatch('build-11y');
|
||||
});
|
||||
|
||||
it('=> [EDGE CASE] should UPDATE instead of CREATE record for existing id', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
// id-10 and id-11 exist but yet the optType is CREATE. The operator should then prepareUpdate the records instead of prepareCreate
|
||||
await DataOperator.handleIsolatedEntityData({
|
||||
optType: OperationType.CREATE,
|
||||
tableName: IsolatedEntities.APP,
|
||||
values: [
|
||||
{buildNumber: 'build-10x', createdAt: 1, id: 'id-10', versionNumber: 'version-10'},
|
||||
{buildNumber: 'build-11x', createdAt: 1, id: 'id-11', versionNumber: 'version-11'},
|
||||
],
|
||||
});
|
||||
|
||||
const records = await defaultDB!.collections.get(APP).query(Q.where('id', Q.oneOf(['id-10', 'id-11']))).fetch() as App[];
|
||||
|
||||
// Verify if the buildNumber for those two record has been updated
|
||||
expect(records[0].buildNumber).toMatch('build-10x');
|
||||
expect(records[1].buildNumber).toMatch('build-11x');
|
||||
});
|
||||
|
||||
it('=> [EDGE CASE] should CREATE instead of UPDATE record for non-existing id', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
// id-15 and id-16 do not exist but yet the optType is UPDATE. The operator should then prepareCreate the records instead of prepareUpdate
|
||||
await DataOperator.handleIsolatedEntityData({
|
||||
optType: OperationType.UPDATE,
|
||||
tableName: IsolatedEntities.APP,
|
||||
values: [
|
||||
{buildNumber: 'build-10x', createdAt: 1, id: 'id-15', versionNumber: 'version-10'},
|
||||
{buildNumber: 'build-11x', createdAt: 1, id: 'id-16', versionNumber: 'version-11'},
|
||||
],
|
||||
});
|
||||
|
||||
const records = await defaultDB!.collections.get(APP).query(Q.where('id', Q.oneOf(['id-15', 'id-16']))).fetch() as App[];
|
||||
|
||||
// Verify if the buildNumber for those two record has been created
|
||||
expect(records[0].buildNumber).toMatch('build-10x');
|
||||
expect(records[1].buildNumber).toMatch('build-11x');
|
||||
});
|
||||
|
||||
it('=> should use operateAppRecord for APP entity in handleBaseData', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'handleBaseData');
|
||||
|
||||
const data = {
|
||||
optType: OperationType.CREATE,
|
||||
tableName: IsolatedEntities.APP,
|
||||
values: [
|
||||
{buildNumber: 'build-10x', createdAt: 1, id: 'id-21', versionNumber: 'version-10'},
|
||||
{buildNumber: 'build-11y', createdAt: 1, id: 'id-22', versionNumber: 'version-11'},
|
||||
],
|
||||
};
|
||||
|
||||
await DataOperator.handleIsolatedEntityData(data);
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledWith({...data, recordOperator: operateAppRecord});
|
||||
});
|
||||
|
||||
it('=> should use operateGlobalRecord for GLOBAL entity in handleBaseData', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'handleBaseData');
|
||||
|
||||
const data = {
|
||||
optType: OperationType.CREATE,
|
||||
tableName: IsolatedEntities.GLOBAL,
|
||||
values: {id: 'global-1-id', name: 'global-1-name', value: 'global-1-value'},
|
||||
};
|
||||
|
||||
await DataOperator.handleIsolatedEntityData(data);
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledWith({...data, recordOperator: operateGlobalRecord});
|
||||
});
|
||||
|
||||
it('=> should use operateServersRecord for SERVERS entity in handleBaseData', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'handleBaseData');
|
||||
|
||||
const data = {
|
||||
optType: OperationType.CREATE,
|
||||
tableName: IsolatedEntities.SERVERS,
|
||||
values: {
|
||||
dbPath: 'server.db',
|
||||
displayName: 'community',
|
||||
id: 'server-id-1',
|
||||
mentionCount: 0,
|
||||
unreadCount: 0,
|
||||
url: 'https://community.mattermost.com',
|
||||
},
|
||||
};
|
||||
|
||||
await DataOperator.handleIsolatedEntityData(data);
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledWith({...data, recordOperator: operateServersRecord});
|
||||
});
|
||||
|
||||
it('=> should use operateCustomEmojiRecord for CUSTOM_EMOJI entity in handleBaseData', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'handleBaseData');
|
||||
|
||||
const data = {
|
||||
optType: OperationType.CREATE,
|
||||
tableName: IsolatedEntities.CUSTOM_EMOJI,
|
||||
values: {
|
||||
id: 'custom-emoji-id-1',
|
||||
name: 'custom-emoji-1',
|
||||
},
|
||||
};
|
||||
|
||||
await DataOperator.handleIsolatedEntityData(data);
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledWith({...data, recordOperator: operateCustomEmojiRecord});
|
||||
});
|
||||
|
||||
it('=> should use operateRoleRecord for ROLE entity in handleBaseData', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'handleBaseData');
|
||||
|
||||
const data = {
|
||||
optType: OperationType.CREATE,
|
||||
tableName: IsolatedEntities.ROLE,
|
||||
values: {
|
||||
id: 'custom-emoji-id-1',
|
||||
name: 'custom-emoji-1',
|
||||
permissions: ['custom-emoji-1'],
|
||||
},
|
||||
};
|
||||
|
||||
await DataOperator.handleIsolatedEntityData(data);
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledWith({...data, recordOperator: operateRoleRecord});
|
||||
});
|
||||
|
||||
it('=> should use operateSystemRecord for SYSTEM entity in handleBaseData', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'handleBaseData');
|
||||
|
||||
const data = {
|
||||
optType: OperationType.CREATE,
|
||||
tableName: IsolatedEntities.SYSTEM,
|
||||
values: {id: 'system-id-1', name: 'system-1', value: 'system-1'},
|
||||
};
|
||||
|
||||
await DataOperator.handleIsolatedEntityData(data);
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledWith({...data, recordOperator: operateSystemRecord});
|
||||
});
|
||||
|
||||
it('=> should use operateTermsOfServiceRecord for TERMS_OF_SERVICE entity in handleBaseData', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'handleBaseData');
|
||||
|
||||
const data = {
|
||||
optType: OperationType.CREATE,
|
||||
tableName: IsolatedEntities.TERMS_OF_SERVICE,
|
||||
values: {id: 'tos-1', acceptedAt: 1},
|
||||
};
|
||||
|
||||
await DataOperator.handleIsolatedEntityData(data);
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledWith({...data, recordOperator: operateTermsOfServiceRecord});
|
||||
});
|
||||
|
||||
it('=> should not call handleBaseData if tableName is invalid', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
|
||||
const spyOnHandleBase = jest.spyOn(DataOperator as any, 'handleBaseData');
|
||||
|
||||
const data = {
|
||||
optType: OperationType.CREATE,
|
||||
tableName: 'INVALID_TABLE_NAME',
|
||||
values: {id: 'tos-1', acceptedAt: 1},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
await DataOperator.handleIsolatedEntityData(data);
|
||||
|
||||
expect(spyOnHandleBase).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
123
app/database/admin/data_operator/utils/index.ts
Normal file
123
app/database/admin/data_operator/utils/index.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {ChainPosts, SanitizePosts, SanitizeReactions, RawPost, RawReaction} from '@typings/database/database';
|
||||
import Reaction from '@typings/database/reaction';
|
||||
|
||||
const {REACTION} = MM_TABLES.SERVER;
|
||||
|
||||
/**
|
||||
* sanitizePosts: Creates arrays of ordered and unordered posts. Unordered posts are those posts that are not
|
||||
* present in the orders array
|
||||
* @param {SanitizePosts} sanitizePosts
|
||||
* @param {RawPost[]} sanitizePosts.posts
|
||||
* @param {string[]} sanitizePosts.orders
|
||||
*/
|
||||
export const sanitizePosts = ({posts, orders}: SanitizePosts) => {
|
||||
const orderedPosts:RawPost[] = [];
|
||||
const unOrderedPosts:RawPost[] = [];
|
||||
|
||||
posts.forEach((post) => {
|
||||
if (post?.id && orders.includes(post.id)) {
|
||||
orderedPosts.push(post);
|
||||
} else {
|
||||
unOrderedPosts.push(post);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
orderedPosts,
|
||||
unOrderedPosts,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 {ChainPosts} chainPosts
|
||||
* @param {string[]} chainPosts.orders
|
||||
* @param {RawPost[]} chainPosts.rawPosts
|
||||
* @param {string} chainPosts.previousPostId
|
||||
* @returns {RawPost[]}
|
||||
*/
|
||||
export const createPostsChain = ({orders, rawPosts, previousPostId = ''}: ChainPosts) => {
|
||||
const posts: RawPost[] = [];
|
||||
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({...post, prev_post_id: previousPostId});
|
||||
} else {
|
||||
posts.push({...post, prev_post_id: orders[orderIndex - 1]});
|
||||
}
|
||||
});
|
||||
|
||||
return posts;
|
||||
};
|
||||
|
||||
/**
|
||||
* sanitizeReactions: Treats reactions happening on a Post. For example, a user can add/remove an emoji. Hence, this function
|
||||
* tell us which reactions to create/delete in the Reaction table and which custom-emoji to create in our database.
|
||||
* For more information, please have a look at https://community.mattermost.com/core/pl/rq9e8jnonpyrmnyxpuzyc4d6ko
|
||||
* @param {SanitizeReactions} sanitizeReactions
|
||||
* @param {Database} sanitizeReactions.database
|
||||
* @param {string} sanitizeReactions.post_id
|
||||
* @param {RawReaction[]} sanitizeReactions.rawReactions
|
||||
* @returns {Promise<{createReactions: RawReaction[], createEmojis: {name: string}[], deleteReactions: Reaction[]}>}
|
||||
*/
|
||||
export const sanitizeReactions = async ({database, post_id, rawReactions}: SanitizeReactions) => {
|
||||
const reactions = (await database.collections.
|
||||
get(REACTION).
|
||||
query(Q.where('post_id', post_id)).
|
||||
fetch()) as Reaction[];
|
||||
|
||||
// similarObjects: Contains objects that are in both the RawReaction array and in the Reaction entity
|
||||
const similarObjects: Reaction[] = [];
|
||||
|
||||
const createReactions: RawReaction[] = [];
|
||||
|
||||
const emojiSet = new Set();
|
||||
|
||||
for (let i = 0; i < rawReactions.length; i++) {
|
||||
const rawReaction = rawReactions[i] as RawReaction;
|
||||
|
||||
// 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 (idxPresent === -1) {
|
||||
// So, we don't have a similar Reaction object. That one is new...so we'll create it
|
||||
createReactions.push(rawReaction);
|
||||
|
||||
// If that reaction is new, that implies that the emoji might also be new
|
||||
emojiSet.add(rawReaction.emoji_name);
|
||||
} else {
|
||||
// we have a similar object in both reactions and rawReactions; we'll pop it out from both arrays
|
||||
similarObjects.push(reactions[idxPresent]);
|
||||
}
|
||||
}
|
||||
|
||||
// finding out elements to delete using array subtract
|
||||
const deleteReactions = reactions.
|
||||
filter((reaction) => !similarObjects.includes(reaction)).
|
||||
map((outCast) => outCast.prepareDestroyPermanently());
|
||||
|
||||
const createEmojis = Array.from(emojiSet).map((emoji) => {
|
||||
return {name: emoji};
|
||||
});
|
||||
|
||||
return {createReactions, createEmojis, deleteReactions};
|
||||
};
|
||||
210
app/database/admin/data_operator/utils/mock.ts
Normal file
210
app/database/admin/data_operator/utils/mock.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const mockedPosts = {
|
||||
order: [
|
||||
'8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
'8fcnk3p1jt8mmkaprgajoxz115a',
|
||||
'3y3w3a6gkbg73bnj3xund9o5ic',
|
||||
'4btbnmticjgw7ewd3qopmpiwqw',
|
||||
],
|
||||
posts: {
|
||||
'8swgtrrdiff89jnsiwiip3y1eoe': {
|
||||
id: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
create_at: 1596032651748,
|
||||
update_at: 1596032651748,
|
||||
edit_at: 0,
|
||||
delete_at: 0,
|
||||
is_pinned: false,
|
||||
user_id: 'q3mzxua9zjfczqakxdkowc6u6yy',
|
||||
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
|
||||
root_id: 'ps81iqbesfby8jayz7owg4yypoo',
|
||||
parent_id: 'ps81iqbddesfby8jayz7owg4yypoo',
|
||||
original_id: '',
|
||||
message: "I'll second these kudos! Thanks m!",
|
||||
type: '',
|
||||
props: {},
|
||||
hashtags: '',
|
||||
pending_post_id: '',
|
||||
reply_count: 4,
|
||||
last_reply_at: 0,
|
||||
participants: null,
|
||||
metadata: {},
|
||||
},
|
||||
'8fcnk3p1jt8mmkaprgajoxz115a': {
|
||||
id: '8fcnk3p1jt8mmkaprgajoxz115a',
|
||||
create_at: 1601557961124,
|
||||
update_at: 1601557961124,
|
||||
edit_at: 0,
|
||||
delete_at: 0,
|
||||
is_pinned: false,
|
||||
user_id: 'hy5sq51sebfh58ktrce5ijtcwyy',
|
||||
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
|
||||
root_id: '',
|
||||
parent_id: '',
|
||||
original_id: '',
|
||||
message: 'a added to the channel by j.',
|
||||
type: 'system_add_to_channel',
|
||||
props: {
|
||||
addedUserId: 'z89qsntet7bimd3xddfu7u9ncdaxc',
|
||||
addedUsername: 'a',
|
||||
userId: 'hy5sdfdfq51sebfh58ktrce5ijtcwy',
|
||||
username: 'j',
|
||||
},
|
||||
hashtags: '',
|
||||
pending_post_id: '',
|
||||
reply_count: 0,
|
||||
last_reply_at: 0,
|
||||
participants: null,
|
||||
metadata: {},
|
||||
},
|
||||
'3y3w3a6gkbg73bnj3xund9o5ic': {
|
||||
id: '3y3w3a6gkbg73bnj3xund9o5ic',
|
||||
create_at: 1596213796212,
|
||||
update_at: 1596213796212,
|
||||
edit_at: 0,
|
||||
delete_at: 0,
|
||||
is_pinned: false,
|
||||
user_id: '44ud4m9tqwby3mphzzdwm7h31sr',
|
||||
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
|
||||
root_id: 'ps81iqbewesfby8jayz7owg4yypo',
|
||||
parent_id: 'ps81iqbwesfby8jayz7owg4yypo',
|
||||
original_id: '',
|
||||
message: 'Great work M!',
|
||||
type: '',
|
||||
props: {},
|
||||
hashtags: '',
|
||||
pending_post_id: '',
|
||||
reply_count: 4,
|
||||
last_reply_at: 0,
|
||||
participants: null,
|
||||
metadata: {},
|
||||
},
|
||||
'4btbnmticjgw7ewd3qopmpiwqw': {
|
||||
id: '4btbnmticjgw7ewd3qopmpiwqw',
|
||||
create_at: 1603221654312,
|
||||
update_at: 1603221654312,
|
||||
edit_at: 0,
|
||||
delete_at: 0,
|
||||
is_pinned: false,
|
||||
user_id: 'opihgdf9nby385mnxj7a5jpfsy3e',
|
||||
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
|
||||
root_id: '',
|
||||
parent_id: '',
|
||||
original_id: '',
|
||||
message: 'eannel.',
|
||||
type: 'system_join_channel',
|
||||
props: {
|
||||
username: 'dok',
|
||||
},
|
||||
hashtags: '',
|
||||
pending_post_id: '',
|
||||
reply_count: 0,
|
||||
last_reply_at: 0,
|
||||
participants: null,
|
||||
metadata: {},
|
||||
},
|
||||
'4r9jmr7eqt8dxq3f9woypzurry': {
|
||||
id: '4r9jmr7eqt8dxq3f9woypzurry',
|
||||
create_at: 1608252986811,
|
||||
update_at: 1608323574215,
|
||||
edit_at: 0,
|
||||
delete_at: 0,
|
||||
is_pinned: false,
|
||||
user_id: '1zkzkhh357b4bdfejephjz5u8daw',
|
||||
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
|
||||
root_id: 'a7ebyw883sdftrm884p1qcgt8yw4a',
|
||||
parent_id: 'a7ebyw8q83trm884p1qcgt8yw4a',
|
||||
original_id: '',
|
||||
message: 'Oh off fixed',
|
||||
type: '',
|
||||
props: {},
|
||||
hashtags: '',
|
||||
pending_post_id: '',
|
||||
has_reactions: true,
|
||||
reply_count: 7,
|
||||
last_reply_at: 0,
|
||||
participants: null,
|
||||
metadata: {
|
||||
reactions: [
|
||||
{
|
||||
user_id: 'weeoa9janfdmbgp3wrgcbofxpo',
|
||||
post_id: '4r9jmr7eqt8dxq3f9woypzurry',
|
||||
emoji_name: 'thumbsup',
|
||||
create_at: 1608253011321,
|
||||
update_at: 1608253011321,
|
||||
delete_at: 0,
|
||||
},
|
||||
{
|
||||
user_id: 'dbjk4taox3fn5mxnua7fc5zo6c',
|
||||
post_id: '4r9jmr7eqt8dxq3f9woypzurry',
|
||||
emoji_name: 'thumbsup',
|
||||
create_at: 1608253070704,
|
||||
update_at: 1608253070704,
|
||||
delete_at: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'7mwm4hb7g3bx8e1yumpi5zgwtw': {
|
||||
id: '7mwm4hb7g3bx8e1yumpi5zgwtw',
|
||||
create_at: 1608289643899,
|
||||
update_at: 1608289727232,
|
||||
edit_at: 1608289727232,
|
||||
delete_at: 0,
|
||||
is_pinned: false,
|
||||
user_id: '1zkzkhhk357b4bejephjz5u8daw',
|
||||
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
|
||||
root_id: 'a7ebyw8f83trm884p1qcgt8yw4a',
|
||||
parent_id: 'a7ebyw883trm884p1qcgt8yw4a',
|
||||
original_id: '',
|
||||
message: 'There are many ',
|
||||
type: '',
|
||||
props: {},
|
||||
hashtags: '',
|
||||
pending_post_id: '',
|
||||
reply_count: 7,
|
||||
last_reply_at: 0,
|
||||
participants: null,
|
||||
metadata: {
|
||||
emojis: [
|
||||
{
|
||||
id: 'by3ddj8act7g1icikwehpksi1ycd',
|
||||
create_at: 1580913641769,
|
||||
update_at: 1580913641769,
|
||||
delete_at: 0,
|
||||
creator_id: '4cdprpki7ri81mbx8efixdcsb8jo',
|
||||
name: 'boom2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockedReactions = [
|
||||
{
|
||||
user_id: 'beqkgo4wzbn98kjzjgc1p5n91o',
|
||||
post_id: '8ww8kb1dbpf59fu4d5xhu5nf5w',
|
||||
emoji_name: 'tada',
|
||||
create_at: 1601558322701,
|
||||
update_at: 1601558322701,
|
||||
delete_at: 0,
|
||||
},
|
||||
{
|
||||
user_id: 'z89qsntet7bimd3xu7u9ncdaxc',
|
||||
post_id: '8ww8kb1dbpf59fu4d5xhu5nf5w',
|
||||
emoji_name: 'thanks',
|
||||
create_at: 1601558379209,
|
||||
update_at: 1601558379209,
|
||||
delete_at: 0,
|
||||
},
|
||||
{
|
||||
user_id: 'z89qsntet7bimd3xu7u9ncdaxc',
|
||||
post_id: '8ww8kb1dbpf59fu4d5xhu5nf5w',
|
||||
emoji_name: 'thumbsup',
|
||||
create_at: 1601558413496,
|
||||
update_at: 1601558413496,
|
||||
delete_at: 0,
|
||||
},
|
||||
];
|
||||
116
app/database/admin/data_operator/utils/test.ts
Normal file
116
app/database/admin/data_operator/utils/test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/admin/database_manager';
|
||||
import DataOperator from '@database/admin/data_operator';
|
||||
import {DatabaseType} from '@typings/database/enums';
|
||||
|
||||
import {createPostsChain, sanitizePosts, sanitizeReactions} from './index';
|
||||
import {mockedPosts, mockedReactions} from './mock';
|
||||
|
||||
jest.mock('../../database_manager');
|
||||
|
||||
describe('DataOperator: Utils tests', () => {
|
||||
it('=> sanitizePosts: should filter between ordered and unordered posts', () => {
|
||||
const {orderedPosts, unOrderedPosts} = sanitizePosts({
|
||||
posts: Object.values(mockedPosts.posts),
|
||||
orders: mockedPosts.order,
|
||||
});
|
||||
expect(orderedPosts.length).toBe(4);
|
||||
expect(unOrderedPosts.length).toBe(2);
|
||||
});
|
||||
|
||||
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),
|
||||
previousPostId,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const post1 = chainedOfPosts.find((post) => {
|
||||
return post.id === '8swgtrrdiff89jnsiwiip3y1eoe';
|
||||
});
|
||||
expect(post1).toBeTruthy();
|
||||
expect(post1!.prev_post_id).toBe(previousPostId);
|
||||
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const post2 = chainedOfPosts.find((post) => {
|
||||
return post.id === '8fcnk3p1jt8mmkaprgajoxz115a';
|
||||
});
|
||||
expect(post2).toBeTruthy();
|
||||
expect(post2!.prev_post_id).toBe('8swgtrrdiff89jnsiwiip3y1eoe');
|
||||
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const post3 = chainedOfPosts.find((post) => {
|
||||
return post.id === '3y3w3a6gkbg73bnj3xund9o5ic';
|
||||
});
|
||||
expect(post3).toBeTruthy();
|
||||
expect(post3!.prev_post_id).toBe('8fcnk3p1jt8mmkaprgajoxz115a');
|
||||
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const post4 = chainedOfPosts.find((post) => {
|
||||
return post.id === '4btbnmticjgw7ewd3qopmpiwqw';
|
||||
});
|
||||
expect(post4).toBeTruthy();
|
||||
expect(post4!.prev_post_id).toBe('3y3w3a6gkbg73bnj3xund9o5ic');
|
||||
});
|
||||
|
||||
it('=> sanitizeReactions: should triage between reactions that needs creation/deletion and emojis to be created', async () => {
|
||||
const dbName = 'server_schema_connection';
|
||||
const serverUrl = 'https://appv2.mattermost.com';
|
||||
const database = await DatabaseManager.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl,
|
||||
},
|
||||
});
|
||||
await DatabaseManager.setActiveServerDatabase({
|
||||
displayName: dbName,
|
||||
serverUrl,
|
||||
});
|
||||
|
||||
// we commit one Reaction to our database
|
||||
const prepareRecords = await DataOperator.handleReactions({
|
||||
reactions: [
|
||||
{
|
||||
user_id: 'beqkgo4wzbn98kjzjgc1p5n91o',
|
||||
post_id: '8ww8kb1dbpf59fu4d5xhu5nf5w',
|
||||
emoji_name: 'tada_will_be_removed',
|
||||
create_at: 1601558322701,
|
||||
update_at: 1601558322701,
|
||||
delete_at: 0,
|
||||
},
|
||||
],
|
||||
prepareRowsOnly: true,
|
||||
});
|
||||
|
||||
// Jest in not using the same database instance amongst the Singletons; hence, we are creating the reaction record here
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
await database!.action(async () => {
|
||||
await database!.batch(...prepareRecords);
|
||||
});
|
||||
|
||||
const {
|
||||
createReactions,
|
||||
createEmojis,
|
||||
deleteReactions,
|
||||
} = await sanitizeReactions({
|
||||
database: database!,
|
||||
post_id: '8ww8kb1dbpf59fu4d5xhu5nf5w',
|
||||
rawReactions: mockedReactions,
|
||||
});
|
||||
|
||||
// The reaction with emoji_name 'tada_will_be_removed' will be in the deleteReactions array. This implies that the user who reacted on that post later removed the reaction.
|
||||
expect(deleteReactions.length).toBe(1);
|
||||
expect(deleteReactions[0].emojiName).toBe('tada_will_be_removed');
|
||||
|
||||
expect(createReactions.length).toBe(3);
|
||||
|
||||
expect(createEmojis.length).toBe(3);
|
||||
});
|
||||
});
|
||||
35
app/database/admin/data_operator/wrapper/index.ts
Normal file
35
app/database/admin/data_operator/wrapper/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DataOperator from '@database/admin/data_operator/handlers';
|
||||
import DatabaseManager from '@database/admin/database_manager';
|
||||
import DatabaseConnectionException from '@database/admin/exceptions/database_connection_exception';
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
|
||||
export const createDataOperator = async (serverUrl: string) => {
|
||||
// Retrieves the connection matching serverUrl
|
||||
const connections = await DatabaseManager.retrieveDatabaseInstances([
|
||||
serverUrl,
|
||||
]);
|
||||
|
||||
if (connections?.length) {
|
||||
// finds the connection that corresponds to the serverUrl value
|
||||
const index = connections.findIndex((connection) => {
|
||||
return connection.url === serverUrl;
|
||||
});
|
||||
|
||||
if (!connections?.[index]?.dbInstance) {
|
||||
throw new DatabaseConnectionException(
|
||||
`An instance of a database connection was found but we could not create a connection out of it for url: ${serverUrl}`,
|
||||
);
|
||||
}
|
||||
|
||||
const connection = connections[index].dbInstance as Database;
|
||||
|
||||
return new DataOperator(connection);
|
||||
}
|
||||
|
||||
throw new DatabaseConnectionException(
|
||||
`No database has been registered with this url: ${serverUrl}`,
|
||||
);
|
||||
};
|
||||
353
app/database/admin/data_operator/wrapper/test.ts
Normal file
353
app/database/admin/data_operator/wrapper/test.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/admin/database_manager';
|
||||
import {createDataOperator} from '@database/admin/data_operator/wrapper';
|
||||
import DatabaseConnectionException from '@database/admin/exceptions/database_connection_exception';
|
||||
import {DatabaseType} from '@typings/database/enums';
|
||||
|
||||
jest.mock('@database/admin/database_manager');
|
||||
|
||||
describe('*** DataOperator Wrapper ***', () => {
|
||||
it('=> wrapper should return an instance of DataOperator ', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const serverUrl = 'https://wrapper.mattermost.com';
|
||||
|
||||
// first we create the connection and save it into default database
|
||||
await DatabaseManager.createDatabaseConnection({
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'community mattermost',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl,
|
||||
},
|
||||
shouldAddToDefaultDatabase: true,
|
||||
});
|
||||
|
||||
const dataOperator = await createDataOperator(serverUrl);
|
||||
|
||||
expect(dataOperator).toBeTruthy();
|
||||
});
|
||||
|
||||
it('=> wrapper should throw an error due to invalid server url', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const serverUrl = 'https://wrapper.mattermost.com';
|
||||
|
||||
// first we create the connection and save it into default database
|
||||
await DatabaseManager.createDatabaseConnection({
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'test_database',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl,
|
||||
},
|
||||
shouldAddToDefaultDatabase: true,
|
||||
});
|
||||
|
||||
await expect(createDataOperator('https://error.com')).rejects.toThrow(
|
||||
'No database has been registered with this url: https://error.com',
|
||||
);
|
||||
|
||||
await expect(createDataOperator('https://error.com')).rejects.toThrow(DatabaseConnectionException);
|
||||
});
|
||||
|
||||
it('=> wrapper to handlePosts [OTHER DATABASE]: should write to Post and its sub-child entities', async () => {
|
||||
expect.assertions(12);
|
||||
|
||||
const posts = [
|
||||
{
|
||||
id: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
create_at: 1596032651747,
|
||||
update_at: 1596032651747,
|
||||
edit_at: 0,
|
||||
delete_at: 0,
|
||||
is_pinned: false,
|
||||
user_id: 'q3mzxua9zjfczqakxdkowc6u6yy',
|
||||
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
|
||||
root_id: '',
|
||||
parent_id: 'ps81iqbddesfby8jayz7owg4yypoo',
|
||||
original_id: '',
|
||||
message: "I'll second these kudos! Thanks m!",
|
||||
type: '',
|
||||
props: {},
|
||||
hashtags: '',
|
||||
pending_post_id: '',
|
||||
reply_count: 4,
|
||||
last_reply_at: 0,
|
||||
participants: null,
|
||||
metadata: {
|
||||
images: {
|
||||
'https://community-release.mattermost.com/api/v4/image?url=https%3A%2F%2Favatars1.githubusercontent.com%2Fu%2F6913320%3Fs%3D400%26v%3D4': {
|
||||
width: 400,
|
||||
height: 400,
|
||||
format: 'png',
|
||||
frame_count: 0,
|
||||
},
|
||||
},
|
||||
reactions: [
|
||||
{
|
||||
user_id: 'njic1w1k5inefp848jwk6oukio',
|
||||
post_id: 'a7ebyw883trm884p1qcgt8yw4a',
|
||||
emoji_name: 'clap',
|
||||
create_at: 1608252965442,
|
||||
update_at: 1608252965442,
|
||||
delete_at: 0,
|
||||
},
|
||||
],
|
||||
embeds: [
|
||||
{
|
||||
type: 'opengraph',
|
||||
url:
|
||||
'https://github.com/mickmister/mattermost-plugin-default-theme',
|
||||
data: {
|
||||
type: 'object',
|
||||
url:
|
||||
'https://github.com/mickmister/mattermost-plugin-default-theme',
|
||||
title: 'mickmister/mattermost-plugin-default-theme',
|
||||
description:
|
||||
'Contribute to mickmister/mattermost-plugin-default-theme development by creating an account on GitHub.',
|
||||
determiner: '',
|
||||
site_name: 'GitHub',
|
||||
locale: '',
|
||||
locales_alternate: null,
|
||||
images: [
|
||||
{
|
||||
url: '',
|
||||
secure_url:
|
||||
'https://community-release.mattermost.com/api/v4/image?url=https%3A%2F%2Favatars1.githubusercontent.com%2Fu%2F6913320%3Fs%3D400%26v%3D4',
|
||||
type: '',
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
],
|
||||
audios: null,
|
||||
videos: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
emojis: [
|
||||
{
|
||||
id: 'dgwyadacdbbwjc8t357h6hwsrh',
|
||||
create_at: 1502389307432,
|
||||
update_at: 1502389307432,
|
||||
delete_at: 0,
|
||||
creator_id: 'x6sdh1ok1tyd9f4dgq4ybw839a',
|
||||
name: 'thanks',
|
||||
},
|
||||
],
|
||||
files: [
|
||||
{
|
||||
id: 'f1oxe5rtepfs7n3zifb4sso7po',
|
||||
user_id: '89ertha8xpfsumpucqppy5knao',
|
||||
post_id: 'a7ebyw883trm884p1qcgt8yw4a',
|
||||
create_at: 1608270920357,
|
||||
update_at: 1608270920357,
|
||||
delete_at: 0,
|
||||
name: '4qtwrg.jpg',
|
||||
extension: 'jpg',
|
||||
size: 89208,
|
||||
mime_type: 'image/jpeg',
|
||||
width: 500,
|
||||
height: 656,
|
||||
has_preview_image: true,
|
||||
mini_preview:
|
||||
'/9j/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABAAEAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AN/T/iZp+pX15FpUmnwLbXtpJpyy2sQLw8CcBXA+bksCDnHGOaf4W+P3xIshbQ6loB8RrbK11f3FpbBFW3ZwiFGHB2kr25BIOeCPPbX4S3407T7rTdDfxFNIpDyRaw9lsB4OECHGR15yO4GK6fRPhR4sGmSnxAs8NgchNOjvDPsjz8qSHA37cDk5JPPFdlOpTdPlcVt/Ku1lrvr17b67EPnjrH8/626H/9k=',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '8fcnk3p1jt8mmkaprgajoxz115a',
|
||||
create_at: 1596104683748,
|
||||
update_at: 1596104683748,
|
||||
edit_at: 0,
|
||||
delete_at: 0,
|
||||
is_pinned: false,
|
||||
user_id: 'hy5sq51sebfh58ktrce5ijtcwyy',
|
||||
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
|
||||
root_id: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
parent_id: '',
|
||||
original_id: '',
|
||||
message: 'a added to the channel by j.',
|
||||
type: 'system_add_to_channel',
|
||||
props: {
|
||||
addedUserId: 'z89qsntet7bimd3xddfu7u9ncdaxc',
|
||||
addedUsername: 'a',
|
||||
userId: 'hy5sdfdfq51sebfh58ktrce5ijtcwy',
|
||||
username: 'j',
|
||||
},
|
||||
hashtags: '',
|
||||
pending_post_id: '',
|
||||
reply_count: 0,
|
||||
last_reply_at: 0,
|
||||
participants: null,
|
||||
metadata: {},
|
||||
},
|
||||
{
|
||||
id: '3y3w3a6gkbg73bnj3xund9o5ic',
|
||||
create_at: 1596277483749,
|
||||
update_at: 1596277483749,
|
||||
edit_at: 0,
|
||||
delete_at: 0,
|
||||
is_pinned: false,
|
||||
user_id: '44ud4m9tqwby3mphzzdwm7h31sr',
|
||||
channel_id: 'xxoq1p6bqg7dkxb3kj1mcjoungw',
|
||||
root_id: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
parent_id: 'ps81iqbwesfby8jayz7owg4yypo',
|
||||
original_id: '',
|
||||
message: 'Great work M!',
|
||||
type: '',
|
||||
props: {},
|
||||
hashtags: '',
|
||||
pending_post_id: '',
|
||||
reply_count: 4,
|
||||
last_reply_at: 0,
|
||||
participants: null,
|
||||
metadata: {},
|
||||
},
|
||||
];
|
||||
|
||||
// create connection to other server in default db
|
||||
await DatabaseManager.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'other_server',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: 'https://appv1.mattermost.com',
|
||||
},
|
||||
});
|
||||
|
||||
const dataOperator = await createDataOperator('https://appv1.mattermost.com');
|
||||
|
||||
const spyOnHandleReactions = jest.spyOn(dataOperator as any, 'handleReactions');
|
||||
const spyOnHandleFiles = jest.spyOn(dataOperator as any, 'handleFiles');
|
||||
const spyOnHandlePostMetadata = jest.spyOn(dataOperator as any, 'handlePostMetadata');
|
||||
const spyOnHandleIsolatedEntity = jest.spyOn(dataOperator as any, 'handleIsolatedEntity');
|
||||
const spyOnHandlePostsInThread = jest.spyOn(dataOperator as any, 'handlePostsInThread');
|
||||
const spyOnHandlePostsInChannel = jest.spyOn(dataOperator as any, 'handlePostsInChannel');
|
||||
|
||||
// handlePosts will in turn call handlePostsInThread
|
||||
await dataOperator.handlePosts({
|
||||
orders: [
|
||||
'8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
'8fcnk3p1jt8mmkaprgajoxz115a',
|
||||
'3y3w3a6gkbg73bnj3xund9o5ic',
|
||||
],
|
||||
values: posts,
|
||||
previousPostId: '',
|
||||
});
|
||||
|
||||
expect(spyOnHandleReactions).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnHandleReactions).toHaveBeenCalledWith({
|
||||
reactions: [
|
||||
{
|
||||
user_id: 'njic1w1k5inefp848jwk6oukio',
|
||||
post_id: 'a7ebyw883trm884p1qcgt8yw4a',
|
||||
emoji_name: 'clap',
|
||||
create_at: 1608252965442,
|
||||
update_at: 1608252965442,
|
||||
delete_at: 0,
|
||||
},
|
||||
],
|
||||
prepareRowsOnly: true,
|
||||
});
|
||||
|
||||
expect(spyOnHandleFiles).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnHandleFiles).toHaveBeenCalledWith({
|
||||
files: [
|
||||
{
|
||||
id: 'f1oxe5rtepfs7n3zifb4sso7po',
|
||||
user_id: '89ertha8xpfsumpucqppy5knao',
|
||||
post_id: 'a7ebyw883trm884p1qcgt8yw4a',
|
||||
create_at: 1608270920357,
|
||||
update_at: 1608270920357,
|
||||
delete_at: 0,
|
||||
name: '4qtwrg.jpg',
|
||||
extension: 'jpg',
|
||||
size: 89208,
|
||||
mime_type: 'image/jpeg',
|
||||
width: 500,
|
||||
height: 656,
|
||||
has_preview_image: true,
|
||||
mini_preview:
|
||||
'/9j/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABAAEAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AN/T/iZp+pX15FpUmnwLbXtpJpyy2sQLw8CcBXA+bksCDnHGOaf4W+P3xIshbQ6loB8RrbK11f3FpbBFW3ZwiFGHB2kr25BIOeCPPbX4S3407T7rTdDfxFNIpDyRaw9lsB4OECHGR15yO4GK6fRPhR4sGmSnxAs8NgchNOjvDPsjz8qSHA37cDk5JPPFdlOpTdPlcVt/Ku1lrvr17b67EPnjrH8/626H/9k=',
|
||||
},
|
||||
],
|
||||
prepareRowsOnly: true,
|
||||
});
|
||||
|
||||
expect(spyOnHandlePostMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnHandlePostMetadata).toHaveBeenCalledWith({
|
||||
embeds: [
|
||||
{
|
||||
embed: [
|
||||
{
|
||||
type: 'opengraph',
|
||||
url: 'https://github.com/mickmister/mattermost-plugin-default-theme',
|
||||
data: {
|
||||
type: 'object',
|
||||
url: 'https://github.com/mickmister/mattermost-plugin-default-theme',
|
||||
title: 'mickmister/mattermost-plugin-default-theme',
|
||||
description: 'Contribute to mickmister/mattermost-plugin-default-theme development by creating an account on GitHub.',
|
||||
determiner: '',
|
||||
site_name: 'GitHub',
|
||||
locale: '',
|
||||
locales_alternate: null,
|
||||
images: [
|
||||
{
|
||||
url: '',
|
||||
secure_url: 'https://community-release.mattermost.com/api/v4/image?url=https%3A%2F%2Favatars1.githubusercontent.com%2Fu%2F6913320%3Fs%3D400%26v%3D4',
|
||||
type: '',
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
],
|
||||
audios: null,
|
||||
videos: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
postId: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
},
|
||||
],
|
||||
images: [
|
||||
{
|
||||
images: {
|
||||
'https://community-release.mattermost.com/api/v4/image?url=https%3A%2F%2Favatars1.githubusercontent.com%2Fu%2F6913320%3Fs%3D400%26v%3D4': {
|
||||
width: 400,
|
||||
height: 400,
|
||||
format: 'png',
|
||||
frame_count: 0,
|
||||
},
|
||||
},
|
||||
postId: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
},
|
||||
],
|
||||
prepareRowsOnly: true,
|
||||
});
|
||||
|
||||
expect(spyOnHandleIsolatedEntity).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnHandleIsolatedEntity).toHaveBeenCalledWith({
|
||||
tableName: 'CustomEmoji',
|
||||
values: [
|
||||
{
|
||||
id: 'dgwyadacdbbwjc8t357h6hwsrh',
|
||||
create_at: 1502389307432,
|
||||
update_at: 1502389307432,
|
||||
delete_at: 0,
|
||||
creator_id: 'x6sdh1ok1tyd9f4dgq4ybw839a',
|
||||
name: 'thanks',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(spyOnHandlePostsInThread).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnHandlePostsInThread).toHaveBeenCalledWith([{earliest: 1596032651747, post_id: '8swgtrrdiff89jnsiwiip3y1eoe'}]);
|
||||
|
||||
expect(spyOnHandlePostsInChannel).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnHandlePostsInChannel).toHaveBeenCalledWith(posts.slice(0, 3));
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Model, Q} from '@nozbe/watermelondb';
|
||||
import {Class} from '@nozbe/watermelondb/utils/common';
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
import LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import IServers from '@typings/database/servers';
|
||||
import type {DBInstance, DefaultNewServer, MMDatabaseConnection} from '@typings/database/database';
|
||||
|
||||
import DefaultMigration from '../../../default/migration';
|
||||
import {App, Global, Servers} from '../../../default/models';
|
||||
import DefaultMigration from '@database/default/migration';
|
||||
import {App, Global, Servers} from '@database/default/models';
|
||||
import {defaultSchema} from '@database/default/schema';
|
||||
import ServerMigration from '@database/server/migration';
|
||||
import {
|
||||
Channel,
|
||||
ChannelInfo,
|
||||
@@ -40,33 +38,22 @@ import {
|
||||
TeamSearchHistory,
|
||||
TermsOfService,
|
||||
User,
|
||||
} from '../../../server/models';
|
||||
import {defaultSchema} from '../../../default/schema';
|
||||
import ServerMigration from '../../../server/migration';
|
||||
import {serverSchema} from '../../../server/schema';
|
||||
} from '@database/server/models';
|
||||
import {serverSchema} from '@database/server/schema';
|
||||
import logger from '@nozbe/watermelondb/utils/common/logger';
|
||||
import type {DatabaseInstance, DefaultNewServer, DatabaseConnection, Models, ActiveServerDatabase} from '@typings/database/database';
|
||||
import {DatabaseType} from '@typings/database/enums';
|
||||
import IServers from '@typings/database/servers';
|
||||
|
||||
const {SERVERS} = MM_TABLES.DEFAULT;
|
||||
|
||||
type Models = Class<Model>[]
|
||||
|
||||
// The elements needed to create a new connection
|
||||
type DatabaseConnection = {
|
||||
databaseConnection: MMDatabaseConnection, shouldAddToDefaultDatabase
|
||||
: boolean
|
||||
}
|
||||
|
||||
// The elements required to switch to another active server database
|
||||
type ActiveServerDatabase = { displayName: string, serverUrl: string }
|
||||
|
||||
// The only two types of databases in the app
|
||||
export enum DatabaseType {
|
||||
DEFAULT,
|
||||
SERVER
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
logger.silence();
|
||||
|
||||
class DatabaseManager {
|
||||
private activeDatabase: DBInstance;
|
||||
private defaultDatabase: DBInstance;
|
||||
private activeDatabase: DatabaseInstance;
|
||||
private defaultDatabase: DatabaseInstance;
|
||||
private readonly defaultModels: Models;
|
||||
private readonly iOSAppGroupDatabase: string | null;
|
||||
private readonly androidFilesDirectory: string | null;
|
||||
@@ -74,10 +61,36 @@ class DatabaseManager {
|
||||
|
||||
constructor() {
|
||||
this.defaultModels = [App, Global, Servers];
|
||||
this.serverModels = [Channel, ChannelInfo, ChannelMembership, CustomEmoji, Draft, File, Group, GroupMembership,
|
||||
GroupsInChannel, GroupsInTeam, MyChannel, MyChannelSettings, MyTeam, Post, PostMetadata, PostsInChannel,
|
||||
PostsInThread, Preference, Reaction, Role, SlashCommand, System, Team, TeamChannelHistory, TeamMembership,
|
||||
TeamSearchHistory, TermsOfService, User];
|
||||
this.serverModels = [
|
||||
Channel,
|
||||
ChannelInfo,
|
||||
ChannelMembership,
|
||||
CustomEmoji,
|
||||
Draft,
|
||||
File,
|
||||
Group,
|
||||
GroupMembership,
|
||||
GroupsInChannel,
|
||||
GroupsInTeam,
|
||||
MyChannel,
|
||||
MyChannelSettings,
|
||||
MyTeam,
|
||||
Post,
|
||||
PostMetadata,
|
||||
PostsInChannel,
|
||||
PostsInThread,
|
||||
Preference,
|
||||
Reaction,
|
||||
Role,
|
||||
SlashCommand,
|
||||
System,
|
||||
Team,
|
||||
TeamChannelHistory,
|
||||
TeamMembership,
|
||||
TeamSearchHistory,
|
||||
TermsOfService,
|
||||
User,
|
||||
];
|
||||
|
||||
this.iOSAppGroupDatabase = null;
|
||||
this.androidFilesDirectory = null;
|
||||
@@ -86,21 +99,21 @@ class DatabaseManager {
|
||||
/**
|
||||
* createDatabaseConnection: Creates database connection and registers the new connection into the default database. However,
|
||||
* if a database connection could not be created, it will return undefined.
|
||||
* @param {MMDatabaseConnection} databaseConnection
|
||||
* @param {DatabaseConfigs} databaseConnection
|
||||
* @param {boolean} shouldAddToDefaultDatabase
|
||||
*
|
||||
* @returns {Promise<DBInstance>}
|
||||
* @returns {Promise<DatabaseInstance>}
|
||||
*/
|
||||
createDatabaseConnection = async ({
|
||||
databaseConnection,
|
||||
configs,
|
||||
shouldAddToDefaultDatabase = true,
|
||||
}: DatabaseConnection): Promise<DBInstance> => {
|
||||
}: DatabaseConnection): Promise<DatabaseInstance> => {
|
||||
const {
|
||||
actionsEnabled = true,
|
||||
dbName = 'default',
|
||||
dbType = DatabaseType.DEFAULT,
|
||||
serverUrl = undefined,
|
||||
} = databaseConnection;
|
||||
} = configs;
|
||||
|
||||
try {
|
||||
const databaseName = dbType === DatabaseType.DEFAULT ? 'default' : dbName;
|
||||
@@ -118,7 +131,11 @@ class DatabaseManager {
|
||||
|
||||
// Registers the new server connection into the DEFAULT database
|
||||
if (serverUrl && shouldAddToDefaultDatabase) {
|
||||
await this.addServerToDefaultDatabase({databaseFilePath: databaseName, displayName: dbName, serverUrl});
|
||||
await this.addServerToDefaultDatabase({
|
||||
databaseFilePath: databaseName,
|
||||
displayName: dbName,
|
||||
serverUrl,
|
||||
});
|
||||
}
|
||||
return new Database({adapter, actionsEnabled, modelClasses});
|
||||
} catch (e) {
|
||||
@@ -139,7 +156,7 @@ class DatabaseManager {
|
||||
const isServerPresent = await this.isServerPresent(serverUrl);
|
||||
|
||||
this.activeDatabase = await this.createDatabaseConnection({
|
||||
databaseConnection: {
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: displayName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
@@ -160,14 +177,14 @@ class DatabaseManager {
|
||||
return server.url === serverUrl;
|
||||
});
|
||||
return existingServer && existingServer.length > 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* getActiveServerDatabase: The DatabaseManager should be the only one setting the active database. Hence, we have made the activeDatabase property private.
|
||||
* Use this getter method to retrieve the active database if it has been set in your code.
|
||||
* @returns {DBInstance}
|
||||
* @returns {DatabaseInstance}
|
||||
*/
|
||||
getActiveServerDatabase = (): DBInstance => {
|
||||
getActiveServerDatabase = (): DatabaseInstance => {
|
||||
return this.activeDatabase;
|
||||
};
|
||||
|
||||
@@ -175,7 +192,7 @@ class DatabaseManager {
|
||||
* getDefaultDatabase : Returns the default database.
|
||||
* @returns {Database} default database
|
||||
*/
|
||||
getDefaultDatabase = async (): Promise<DBInstance> => {
|
||||
getDefaultDatabase = async (): Promise<DatabaseInstance> => {
|
||||
if (!this.defaultDatabase) {
|
||||
await this.setDefaultDatabase();
|
||||
}
|
||||
@@ -187,9 +204,11 @@ class DatabaseManager {
|
||||
* and return them to the caller.
|
||||
*
|
||||
* @param {string[]} serverUrls
|
||||
* @returns {Promise<{url: string, dbInstance: DBInstance}[] | null>}
|
||||
* @returns {Promise<{url: string, dbInstance: DatabaseInstance}[] | null>}
|
||||
*/
|
||||
retrieveDatabaseInstances = async (serverUrls?: string[]): Promise<{ url: string, dbInstance: DBInstance }[] | null> => {
|
||||
retrieveDatabaseInstances = async (
|
||||
serverUrls?: string[],
|
||||
): Promise<{ url: string; dbInstance: DatabaseInstance }[] | null> => {
|
||||
if (serverUrls?.length) {
|
||||
// Retrieve all server records from the default db
|
||||
const allServers = await this.getAllServers();
|
||||
@@ -203,16 +222,15 @@ class DatabaseManager {
|
||||
if (servers.length) {
|
||||
const databasePromises = servers.map(async (server: IServers) => {
|
||||
const {displayName, url} = server;
|
||||
const databaseConnection = {
|
||||
actionsEnabled: true,
|
||||
dbName: displayName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: url,
|
||||
};
|
||||
|
||||
// Since we are retrieving existing URL ( and so database connections ) from the 'DEFAULT' database, shouldAddToDefaultDatabase is set to false
|
||||
const dbInstance = await this.createDatabaseConnection({
|
||||
databaseConnection,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: displayName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: url,
|
||||
},
|
||||
shouldAddToDefaultDatabase: false,
|
||||
});
|
||||
|
||||
@@ -239,7 +257,10 @@ class DatabaseManager {
|
||||
let server: IServers;
|
||||
|
||||
if (defaultDB) {
|
||||
const serversRecords = await defaultDB.collections.get(SERVERS).query(Q.where('url', serverUrl)).fetch() as IServers[];
|
||||
const serversRecords = (await defaultDB.collections.
|
||||
get(SERVERS).
|
||||
query(Q.where('url', serverUrl)).
|
||||
fetch()) as IServers[];
|
||||
server = serversRecords?.[0] ?? undefined;
|
||||
|
||||
if (server) {
|
||||
@@ -266,17 +287,17 @@ class DatabaseManager {
|
||||
private getAllServers = async () => {
|
||||
// Retrieve all server records from the default db
|
||||
const defaultDatabase = await this.getDefaultDatabase();
|
||||
const allServers = defaultDatabase && await defaultDatabase.collections.get(MM_TABLES.DEFAULT.SERVERS).query().fetch() as IServers[];
|
||||
const allServers = defaultDatabase && ((await defaultDatabase.collections.get(MM_TABLES.DEFAULT.SERVERS).query().fetch()) as IServers[]);
|
||||
return allServers;
|
||||
};
|
||||
|
||||
/**
|
||||
* setDefaultDatabase : Sets the default database.
|
||||
* @returns {Promise<DBInstance>}
|
||||
* @returns {Promise<DatabaseInstance>}
|
||||
*/
|
||||
private setDefaultDatabase = async (): Promise<DBInstance> => {
|
||||
private setDefaultDatabase = async (): Promise<DatabaseInstance> => {
|
||||
this.defaultDatabase = await this.createDatabaseConnection({
|
||||
databaseConnection: {dbName: 'default'},
|
||||
configs: {dbName: 'default'},
|
||||
shouldAddToDefaultDatabase: false,
|
||||
});
|
||||
return this.defaultDatabase;
|
||||
@@ -289,11 +310,7 @@ class DatabaseManager {
|
||||
* @param {string} serverUrl
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private addServerToDefaultDatabase = async ({
|
||||
databaseFilePath,
|
||||
displayName,
|
||||
serverUrl,
|
||||
}: DefaultNewServer) => {
|
||||
private addServerToDefaultDatabase = async ({databaseFilePath, displayName, serverUrl}: DefaultNewServer) => {
|
||||
try {
|
||||
const defaultDatabase = await this.getDefaultDatabase();
|
||||
const isServerPresent = await this.isServerPresent(serverUrl);
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite';
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite';
|
||||
import logger from '@nozbe/watermelondb/utils/common/logger';
|
||||
import {DeviceEventEmitter, Platform} from 'react-native';
|
||||
import {FileSystem} from 'react-native-unimodules';
|
||||
|
||||
import {MIGRATION_EVENTS, MM_TABLES} from '@constants/database';
|
||||
import IServers from '@typings/database/servers';
|
||||
import type {
|
||||
ActiveServerDatabase,
|
||||
DBInstance,
|
||||
DatabaseConnection,
|
||||
DefaultNewServer,
|
||||
MigrationEvents,
|
||||
Models,
|
||||
} from '@typings/database/database';
|
||||
import {deleteIOSDatabase, getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
||||
|
||||
import DefaultMigration from '../../default/migration';
|
||||
import {App, Global, Servers} from '../../default/models';
|
||||
import {defaultSchema} from '../../default/schema';
|
||||
import DefaultMigration from '@database/default/migration';
|
||||
import {App, Global, Servers} from '@database/default/models';
|
||||
import {defaultSchema} from '@database/default/schema';
|
||||
import ServerMigration from '@database/server/migration';
|
||||
import {
|
||||
Channel,
|
||||
ChannelInfo,
|
||||
@@ -50,21 +41,32 @@ import {
|
||||
TeamSearchHistory,
|
||||
TermsOfService,
|
||||
User,
|
||||
} from '../../server/models';
|
||||
import ServerMigration from '../../server/migration';
|
||||
import {serverSchema} from '../../server/schema';
|
||||
} from '@database/server/models';
|
||||
import {serverSchema} from '@database/server/schema';
|
||||
import type {
|
||||
ActiveServerDatabase,
|
||||
DatabaseConnection,
|
||||
DatabaseInstance,
|
||||
DefaultNewServer,
|
||||
MigrationEvents,
|
||||
Models,
|
||||
} from '@typings/database/database';
|
||||
import {DatabaseType} from '@typings/database/enums';
|
||||
import IServers from '@typings/database/servers';
|
||||
import {deleteIOSDatabase, getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
||||
|
||||
const {SERVERS} = MM_TABLES.DEFAULT;
|
||||
|
||||
// The only two types of databases in the app
|
||||
export enum DatabaseType {
|
||||
DEFAULT,
|
||||
SERVER
|
||||
if (!__DEV__) {
|
||||
// To prevent logs leaking in production environment
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
logger.silence();
|
||||
}
|
||||
|
||||
class DatabaseManager {
|
||||
private activeDatabase: DBInstance;
|
||||
private defaultDatabase: DBInstance;
|
||||
private activeDatabase: DatabaseInstance;
|
||||
private defaultDatabase: DatabaseInstance;
|
||||
private readonly defaultModels: Models;
|
||||
private readonly iOSAppGroupDatabase: string | null;
|
||||
private readonly androidFilesDirectory: string | null;
|
||||
@@ -72,10 +74,36 @@ class DatabaseManager {
|
||||
|
||||
constructor() {
|
||||
this.defaultModels = [App, Global, Servers];
|
||||
this.serverModels = [Channel, ChannelInfo, ChannelMembership, CustomEmoji, Draft, File, Group, GroupMembership,
|
||||
GroupsInChannel, GroupsInTeam, MyChannel, MyChannelSettings, MyTeam, Post, PostMetadata, PostsInChannel,
|
||||
PostsInThread, Preference, Reaction, Role, SlashCommand, System, Team, TeamChannelHistory, TeamMembership,
|
||||
TeamSearchHistory, TermsOfService, User];
|
||||
this.serverModels = [
|
||||
Channel,
|
||||
ChannelInfo,
|
||||
ChannelMembership,
|
||||
CustomEmoji,
|
||||
Draft,
|
||||
File,
|
||||
Group,
|
||||
GroupMembership,
|
||||
GroupsInChannel,
|
||||
GroupsInTeam,
|
||||
MyChannel,
|
||||
MyChannelSettings,
|
||||
MyTeam,
|
||||
Post,
|
||||
PostMetadata,
|
||||
PostsInChannel,
|
||||
PostsInThread,
|
||||
Preference,
|
||||
Reaction,
|
||||
Role,
|
||||
SlashCommand,
|
||||
System,
|
||||
Team,
|
||||
TeamChannelHistory,
|
||||
TeamMembership,
|
||||
TeamSearchHistory,
|
||||
TermsOfService,
|
||||
User,
|
||||
];
|
||||
|
||||
this.iOSAppGroupDatabase = Platform.OS === 'ios' ? getIOSAppGroupDetails().appGroupDatabase : null;
|
||||
this.androidFilesDirectory = Platform.OS === 'android' ? FileSystem.documentDirectory : null;
|
||||
@@ -87,18 +115,18 @@ class DatabaseManager {
|
||||
* @param {MMDatabaseConnection} databaseConnection
|
||||
* @param {boolean} shouldAddToDefaultDatabase
|
||||
*
|
||||
* @returns {Promise<DBInstance>}
|
||||
* @returns {Promise<DatabaseInstance>}
|
||||
*/
|
||||
createDatabaseConnection = async ({
|
||||
databaseConnection,
|
||||
configs,
|
||||
shouldAddToDefaultDatabase = true,
|
||||
}: DatabaseConnection): Promise<DBInstance> => {
|
||||
}: DatabaseConnection): Promise<DatabaseInstance> => {
|
||||
const {
|
||||
actionsEnabled = true,
|
||||
dbName = 'default',
|
||||
dbType = DatabaseType.DEFAULT,
|
||||
serverUrl = undefined,
|
||||
} = databaseConnection;
|
||||
} = configs;
|
||||
|
||||
try {
|
||||
const databaseName = dbType === DatabaseType.DEFAULT ? 'default' : dbName;
|
||||
@@ -138,7 +166,7 @@ class DatabaseManager {
|
||||
const isServerPresent = await this.isServerPresent(serverUrl);
|
||||
|
||||
this.activeDatabase = await this.createDatabaseConnection({
|
||||
databaseConnection: {
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: displayName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
@@ -161,14 +189,14 @@ class DatabaseManager {
|
||||
});
|
||||
|
||||
return existingServer && existingServer.length > 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* getActiveServerDatabase: The DatabaseManager should be the only one setting the active database. Hence, we have made the activeDatabase property private.
|
||||
* Use this getter method to retrieve the active database if it has been set in your code.
|
||||
* @returns {DBInstance}
|
||||
* @returns {DatabaseInstance}
|
||||
*/
|
||||
getActiveServerDatabase = (): DBInstance => {
|
||||
getActiveServerDatabase = (): DatabaseInstance => {
|
||||
return this.activeDatabase;
|
||||
};
|
||||
|
||||
@@ -176,7 +204,7 @@ class DatabaseManager {
|
||||
* getDefaultDatabase : Returns the default database.
|
||||
* @returns {Database} default database
|
||||
*/
|
||||
getDefaultDatabase = async (): Promise<DBInstance> => {
|
||||
getDefaultDatabase = async (): Promise<DatabaseInstance> => {
|
||||
if (!this.defaultDatabase) {
|
||||
await this.setDefaultDatabase();
|
||||
}
|
||||
@@ -188,9 +216,11 @@ class DatabaseManager {
|
||||
* and return them to the caller.
|
||||
*
|
||||
* @param {string[]} serverUrls
|
||||
* @returns {Promise<{url: string, dbInstance: DBInstance}[] | null>}
|
||||
* @returns {Promise<{url: string, dbInstance: DatabaseInstance}[] | null>}
|
||||
*/
|
||||
retrieveDatabaseInstances = async (serverUrls?: string[]): Promise<{ url: string, dbInstance: DBInstance }[] | null> => {
|
||||
retrieveDatabaseInstances = async (
|
||||
serverUrls?: string[],
|
||||
): Promise<{ url: string; dbInstance: DatabaseInstance }[] | null> => {
|
||||
if (serverUrls?.length) {
|
||||
// Retrieve all server records from the default db
|
||||
const allServers = await this.getAllServers();
|
||||
@@ -204,16 +234,15 @@ class DatabaseManager {
|
||||
if (servers.length) {
|
||||
const databasePromises = servers.map(async (server: IServers) => {
|
||||
const {displayName, url} = server;
|
||||
const databaseConnection = {
|
||||
actionsEnabled: true,
|
||||
dbName: displayName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: url,
|
||||
};
|
||||
|
||||
// Since we are retrieving existing URL ( and so database connections ) from the 'DEFAULT' database, shouldAddToDefaultDatabase is set to false
|
||||
const dbInstance = await this.createDatabaseConnection({
|
||||
databaseConnection,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: displayName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: url,
|
||||
},
|
||||
shouldAddToDefaultDatabase: false,
|
||||
});
|
||||
|
||||
@@ -240,7 +269,10 @@ class DatabaseManager {
|
||||
let server: IServers;
|
||||
|
||||
if (defaultDB) {
|
||||
const serversRecords = await defaultDB.collections.get(SERVERS).query(Q.where('url', serverUrl)).fetch() as IServers[];
|
||||
const serversRecords = (await defaultDB.collections.
|
||||
get(SERVERS).
|
||||
query(Q.where('url', serverUrl)).
|
||||
fetch()) as IServers[];
|
||||
server = serversRecords?.[0] ?? undefined;
|
||||
|
||||
if (server) {
|
||||
@@ -306,17 +338,19 @@ class DatabaseManager {
|
||||
private getAllServers = async () => {
|
||||
// Retrieve all server records from the default db
|
||||
const defaultDatabase = await this.getDefaultDatabase();
|
||||
const allServers = defaultDatabase && await defaultDatabase.collections.get(MM_TABLES.DEFAULT.SERVERS).query().fetch() as IServers[];
|
||||
const allServers =
|
||||
defaultDatabase &&
|
||||
((await defaultDatabase.collections.get(MM_TABLES.DEFAULT.SERVERS).query().fetch()) as IServers[]);
|
||||
return allServers;
|
||||
};
|
||||
|
||||
/**
|
||||
* setDefaultDatabase : Sets the default database.
|
||||
* @returns {Promise<DBInstance>}
|
||||
* @returns {Promise<DatabaseInstance>}
|
||||
*/
|
||||
private setDefaultDatabase = async (): Promise<DBInstance> => {
|
||||
private setDefaultDatabase = async (): Promise<DatabaseInstance> => {
|
||||
this.defaultDatabase = await this.createDatabaseConnection({
|
||||
databaseConnection: {dbName: 'default'},
|
||||
configs: {dbName: 'default'},
|
||||
shouldAddToDefaultDatabase: false,
|
||||
});
|
||||
return this.defaultDatabase;
|
||||
@@ -329,11 +363,7 @@ class DatabaseManager {
|
||||
* @param {string} serverUrl
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private addServerToDefaultDatabase = async ({
|
||||
databaseFilePath,
|
||||
displayName,
|
||||
serverUrl,
|
||||
}: DefaultNewServer) => {
|
||||
private addServerToDefaultDatabase = async ({databaseFilePath, displayName, serverUrl}: DefaultNewServer) => {
|
||||
try {
|
||||
const defaultDatabase = await this.getDefaultDatabase();
|
||||
const isServerPresent = await this.isServerPresent(serverUrl);
|
||||
@@ -393,4 +423,10 @@ class DatabaseManager {
|
||||
};
|
||||
}
|
||||
|
||||
if (!__DEV__) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
logger.silence();
|
||||
}
|
||||
|
||||
export default new DatabaseManager();
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
import {DBInstance} from '@typings/database/database';
|
||||
import {DatabaseInstance} from '@typings/database/database';
|
||||
import {DatabaseType} from '@typings/database/enums';
|
||||
import IServers from '@typings/database/servers';
|
||||
|
||||
import DatabaseManager, {DatabaseType} from './index';
|
||||
import DatabaseManager from './index';
|
||||
|
||||
jest.mock('./index');
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('*** Database Manager tests ***', () => {
|
||||
|
||||
const connection1 = await DatabaseManager.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
databaseConnection: {
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'community mattermost',
|
||||
dbType: DatabaseType.SERVER,
|
||||
@@ -50,7 +50,7 @@ describe('*** Database Manager tests ***', () => {
|
||||
|
||||
it('=> should switch between active server connections', async () => {
|
||||
expect.assertions(7);
|
||||
let activeServer: DBInstance;
|
||||
let activeServer: DatabaseInstance;
|
||||
let adapter;
|
||||
|
||||
activeServer = await DatabaseManager.getActiveServerDatabase();
|
||||
@@ -139,7 +139,7 @@ describe('*** Database Manager tests ***', () => {
|
||||
const serverUrl = 'https://appv3.mattermost.com';
|
||||
await DatabaseManager.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
databaseConnection: {
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'community mattermost',
|
||||
dbType: DatabaseType.SERVER,
|
||||
@@ -149,7 +149,7 @@ describe('*** Database Manager tests ***', () => {
|
||||
|
||||
await DatabaseManager.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
databaseConnection: {
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'duplicate server',
|
||||
dbType: DatabaseType.SERVER,
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
import {DatabaseType} from '@typings/database/enums';
|
||||
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
||||
|
||||
import DBManager, {DatabaseType} from './index';
|
||||
import DBManager from './index';
|
||||
|
||||
export default async () => {
|
||||
// Test: It should return the iOS App-Group shared directory
|
||||
@@ -24,7 +25,7 @@ export default async () => {
|
||||
const testNewServerConnection = async () => {
|
||||
await DBManager.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
databaseConnection: {
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'community mattermost',
|
||||
dbType: DatabaseType.SERVER,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* DatabaseConnectionException: This error can be thrown whenever an issue arises with the Database
|
||||
*/
|
||||
class DatabaseConnectionException extends Error {
|
||||
tableName?: string;
|
||||
constructor(message: string, tableName?: string) {
|
||||
super(message);
|
||||
this.tableName = tableName;
|
||||
this.name = 'DatabaseConnectionException';
|
||||
}
|
||||
}
|
||||
export default DatabaseConnectionException;
|
||||
15
app/database/admin/exceptions/database_operator_exception.ts
Normal file
15
app/database/admin/exceptions/database_operator_exception.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* DatabaseOperatorException: This exception can be used whenever an issue arises at the operator level. For example, if a required field is missing.
|
||||
*/
|
||||
class DatabaseOperatorException extends Error {
|
||||
error : Error | undefined;
|
||||
constructor(message: string, error?: Error) {
|
||||
super(message);
|
||||
this.name = 'DatabaseOperatorException';
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
export default DatabaseOperatorException;
|
||||
@@ -57,6 +57,9 @@ export default class Post extends Model {
|
||||
/** delete_at : The timestamp to when this post was last archived/deleted */
|
||||
@field('delete_at') deleteAt!: number;
|
||||
|
||||
/** update_at : The timestamp to when this post was last updated on the server */
|
||||
@field('update_at') updateAt!: number;
|
||||
|
||||
/** edit_at : The timestamp to when this post was last edited */
|
||||
@field('edit_at') editAt!: number;
|
||||
|
||||
@@ -79,19 +82,19 @@ export default class Post extends Model {
|
||||
@field('root_id') rootId!: string;
|
||||
|
||||
/** type : Type of props (e.g. system message) */
|
||||
@field('type') type!: string;
|
||||
@field('type') type!: PostType;
|
||||
|
||||
/** user_id : The foreign key of the User who authored this post. */
|
||||
@field('user_id') userId!: string;
|
||||
|
||||
/** props : Additional attributes for this props */
|
||||
@json('props', (rawJson) => rawJson) props!: string;
|
||||
@json('props', (rawJson) => rawJson) props!: object;
|
||||
|
||||
// A draft can be associated with this post for as long as this post is a parent post
|
||||
@lazy draft = this.collections.get(DRAFT).query(Q.on(POST, 'id', this.id)) as Query<Draft>
|
||||
@lazy draft = this.collections.get(DRAFT).query(Q.on(POST, 'id', this.id)) as Query<Draft>;
|
||||
|
||||
/** postsInThread: The thread to which this post is associated */
|
||||
@lazy postsInThread = this.collections.get(POSTS_IN_THREAD).query(Q.on(POST, 'id', this.id)) as Query<PostInThread>
|
||||
@lazy postsInThread = this.collections.get(POSTS_IN_THREAD).query(Q.on(POST, 'id', this.id)) as Query<PostInThread>;
|
||||
|
||||
/** files: All the files associated with this Post */
|
||||
@children(FILE) files!: File[];
|
||||
|
||||
@@ -6,6 +6,7 @@ import {field, immutableRelation, json} from '@nozbe/watermelondb/decorators';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {PostMetadataData, PostMetadataType} from '@typings/database/database';
|
||||
import Post from '@typings/database/post';
|
||||
|
||||
const {POST, POST_METADATA} = MM_TABLES.SERVER;
|
||||
@@ -28,10 +29,10 @@ export default class PostMetadata extends Model {
|
||||
@field('post_id') postId!: string;
|
||||
|
||||
/** type : The type will work in tandem with the value present in the field 'data'. One 'type' for each kind of 'data' */
|
||||
@field('type') type!: PostType;
|
||||
@field('type') type!: PostMetadataType;
|
||||
|
||||
/** data : Different types of data ranging from arrays, emojis, files to images and reactions. */
|
||||
@json('data', (rawJson) => rawJson) data!: PostMetadataTypes;
|
||||
/** data : Different types of data ranging from embeds to images. */
|
||||
@json('data', (rawJson) => rawJson) data!: PostMetadataData;
|
||||
|
||||
/** post: The record representing the POST parent. */
|
||||
@immutableRelation(POST, 'post_id') post!: Relation<Post>;
|
||||
|
||||
@@ -13,6 +13,7 @@ export default tableSchema({
|
||||
{name: 'channel_id', type: 'string', isIndexed: true},
|
||||
{name: 'create_at', type: 'number'},
|
||||
{name: 'delete_at', type: 'number'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'edit_at', type: 'number'},
|
||||
{name: 'is_pinned', type: 'boolean'},
|
||||
{name: 'message', type: 'string'},
|
||||
|
||||
@@ -219,6 +219,7 @@ describe('*** Test schema for SERVER database ***', () => {
|
||||
channel_id: {name: 'channel_id', type: 'string', isIndexed: true},
|
||||
create_at: {name: 'create_at', type: 'number'},
|
||||
delete_at: {name: 'delete_at', type: 'number'},
|
||||
update_at: {name: 'update_at', type: 'number'},
|
||||
edit_at: {name: 'edit_at', type: 'number'},
|
||||
is_pinned: {name: 'is_pinned', type: 'boolean'},
|
||||
message: {name: 'message', type: 'string'},
|
||||
@@ -234,6 +235,7 @@ describe('*** Test schema for SERVER database ***', () => {
|
||||
{name: 'channel_id', type: 'string', isIndexed: true},
|
||||
{name: 'create_at', type: 'number'},
|
||||
{name: 'delete_at', type: 'number'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'edit_at', type: 'number'},
|
||||
{name: 'is_pinned', type: 'boolean'},
|
||||
{name: 'message', type: 'string'},
|
||||
|
||||
@@ -13,69 +13,69 @@ function loadTranslation(locale: string) {
|
||||
let translations;
|
||||
let momentData;
|
||||
switch (locale) {
|
||||
case 'de':
|
||||
translations = require('@assets/i18n/de.json');
|
||||
momentData = require('moment/locale/de');
|
||||
break;
|
||||
case 'es':
|
||||
translations = require('@assets/i18n/es.json');
|
||||
momentData = require('moment/locale/es');
|
||||
break;
|
||||
case 'fr':
|
||||
translations = require('@assets/i18n/fr.json');
|
||||
momentData = require('moment/locale/fr');
|
||||
break;
|
||||
case 'it':
|
||||
translations = require('@assets/i18n/it.json');
|
||||
momentData = require('moment/locale/it');
|
||||
break;
|
||||
case 'ja':
|
||||
translations = require('@assets/i18n/ja.json');
|
||||
momentData = require('moment/locale/ja');
|
||||
break;
|
||||
case 'ko':
|
||||
translations = require('@assets/i18n/ko.json');
|
||||
momentData = require('moment/locale/ko');
|
||||
break;
|
||||
case 'nl':
|
||||
translations = require('@assets/i18n/nl.json');
|
||||
momentData = require('moment/locale/nl');
|
||||
break;
|
||||
case 'pl':
|
||||
translations = require('@assets/i18n/pl.json');
|
||||
momentData = require('moment/locale/pl');
|
||||
break;
|
||||
case 'pt-BR':
|
||||
translations = require('@assets/i18n/pt-BR.json');
|
||||
momentData = require('moment/locale/pt-br');
|
||||
break;
|
||||
case 'ro':
|
||||
translations = require('@assets/i18n/ro.json');
|
||||
momentData = require('moment/locale/ro');
|
||||
break;
|
||||
case 'ru':
|
||||
translations = require('@assets/i18n/ru.json');
|
||||
momentData = require('moment/locale/ru');
|
||||
break;
|
||||
case 'tr':
|
||||
translations = require('@assets/i18n/tr.json');
|
||||
momentData = require('moment/locale/tr');
|
||||
break;
|
||||
case 'uk':
|
||||
translations = require('@assets/i18n/uk.json');
|
||||
momentData = require('moment/locale/uk');
|
||||
break;
|
||||
case 'zh-CN':
|
||||
translations = require('@assets/i18n/zh-CN.json');
|
||||
momentData = require('moment/locale/zh-cn');
|
||||
break;
|
||||
case 'zh-TW':
|
||||
translations = require('@assets/i18n/zh-TW.json');
|
||||
momentData = require('moment/locale/zh-tw');
|
||||
break;
|
||||
default:
|
||||
translations = en;
|
||||
break;
|
||||
case 'de':
|
||||
translations = require('@assets/i18n/de.json');
|
||||
momentData = require('moment/locale/de');
|
||||
break;
|
||||
case 'es':
|
||||
translations = require('@assets/i18n/es.json');
|
||||
momentData = require('moment/locale/es');
|
||||
break;
|
||||
case 'fr':
|
||||
translations = require('@assets/i18n/fr.json');
|
||||
momentData = require('moment/locale/fr');
|
||||
break;
|
||||
case 'it':
|
||||
translations = require('@assets/i18n/it.json');
|
||||
momentData = require('moment/locale/it');
|
||||
break;
|
||||
case 'ja':
|
||||
translations = require('@assets/i18n/ja.json');
|
||||
momentData = require('moment/locale/ja');
|
||||
break;
|
||||
case 'ko':
|
||||
translations = require('@assets/i18n/ko.json');
|
||||
momentData = require('moment/locale/ko');
|
||||
break;
|
||||
case 'nl':
|
||||
translations = require('@assets/i18n/nl.json');
|
||||
momentData = require('moment/locale/nl');
|
||||
break;
|
||||
case 'pl':
|
||||
translations = require('@assets/i18n/pl.json');
|
||||
momentData = require('moment/locale/pl');
|
||||
break;
|
||||
case 'pt-BR':
|
||||
translations = require('@assets/i18n/pt-BR.json');
|
||||
momentData = require('moment/locale/pt-br');
|
||||
break;
|
||||
case 'ro':
|
||||
translations = require('@assets/i18n/ro.json');
|
||||
momentData = require('moment/locale/ro');
|
||||
break;
|
||||
case 'ru':
|
||||
translations = require('@assets/i18n/ru.json');
|
||||
momentData = require('moment/locale/ru');
|
||||
break;
|
||||
case 'tr':
|
||||
translations = require('@assets/i18n/tr.json');
|
||||
momentData = require('moment/locale/tr');
|
||||
break;
|
||||
case 'uk':
|
||||
translations = require('@assets/i18n/uk.json');
|
||||
momentData = require('moment/locale/uk');
|
||||
break;
|
||||
case 'zh-CN':
|
||||
translations = require('@assets/i18n/zh-CN.json');
|
||||
momentData = require('moment/locale/zh-cn');
|
||||
break;
|
||||
case 'zh-TW':
|
||||
translations = require('@assets/i18n/zh-TW.json');
|
||||
momentData = require('moment/locale/zh-tw');
|
||||
break;
|
||||
default:
|
||||
translations = en;
|
||||
break;
|
||||
}
|
||||
|
||||
if (momentData) {
|
||||
|
||||
@@ -81,38 +81,38 @@ class GlobalEventHandler {
|
||||
|
||||
// TODO: Only execute this if there are no more servers
|
||||
switch (Platform.OS) {
|
||||
case 'ios': {
|
||||
const mainPath = FileSystem.documentDirectory?.split('/').slice(0, -1).join('/');
|
||||
const libraryDir = `${mainPath}/Library`;
|
||||
const cookiesDir = `${libraryDir}/Cookies`;
|
||||
const cookies = await FileSystem.getInfoAsync(cookiesDir);
|
||||
const webkitDir = `${libraryDir}/WebKit`;
|
||||
const webkit = await FileSystem.getInfoAsync(webkitDir);
|
||||
case 'ios': {
|
||||
const mainPath = FileSystem.documentDirectory?.split('/').slice(0, -1).join('/');
|
||||
const libraryDir = `${mainPath}/Library`;
|
||||
const cookiesDir = `${libraryDir}/Cookies`;
|
||||
const cookies = await FileSystem.getInfoAsync(cookiesDir);
|
||||
const webkitDir = `${libraryDir}/WebKit`;
|
||||
const webkit = await FileSystem.getInfoAsync(webkitDir);
|
||||
|
||||
if (cookies.exists) {
|
||||
FileSystem.deleteAsync(cookiesDir);
|
||||
if (cookies.exists) {
|
||||
FileSystem.deleteAsync(cookiesDir);
|
||||
}
|
||||
|
||||
if (webkit.exists) {
|
||||
FileSystem.deleteAsync(webkitDir);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (webkit.exists) {
|
||||
FileSystem.deleteAsync(webkitDir);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'android': {
|
||||
const cacheDir = FileSystem.cacheDirectory;
|
||||
const mainPath = cacheDir?.split('/').slice(0, -1).join('/');
|
||||
const cookies = await FileSystem.getInfoAsync(`${mainPath}/app_webview/Cookies`);
|
||||
const cookiesJ = await FileSystem.getInfoAsync(`${mainPath}/app_webview/Cookies-journal`);
|
||||
if (cookies.exists) {
|
||||
FileSystem.deleteAsync(`${mainPath}/app_webview/Cookies`);
|
||||
}
|
||||
|
||||
case 'android': {
|
||||
const cacheDir = FileSystem.cacheDirectory;
|
||||
const mainPath = cacheDir?.split('/').slice(0, -1).join('/');
|
||||
const cookies = await FileSystem.getInfoAsync(`${mainPath}/app_webview/Cookies`);
|
||||
const cookiesJ = await FileSystem.getInfoAsync(`${mainPath}/app_webview/Cookies-journal`);
|
||||
if (cookies.exists) {
|
||||
FileSystem.deleteAsync(`${mainPath}/app_webview/Cookies`);
|
||||
if (cookiesJ.exists) {
|
||||
FileSystem.deleteAsync(`${mainPath}/app_webview/Cookies-journal`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (cookiesJ.exists) {
|
||||
FileSystem.deleteAsync(`${mainPath}/app_webview/Cookies-journal`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -111,33 +111,33 @@ class PushNotifications {
|
||||
|
||||
if (payload) {
|
||||
switch (payload.type) {
|
||||
case NOTIFICATION_TYPE.CLEAR:
|
||||
case NOTIFICATION_TYPE.CLEAR:
|
||||
// Mark the channel as read
|
||||
break;
|
||||
case NOTIFICATION_TYPE.MESSAGE:
|
||||
break;
|
||||
case NOTIFICATION_TYPE.MESSAGE:
|
||||
// fetch the posts for the channel
|
||||
|
||||
if (foreground) {
|
||||
if (foreground) {
|
||||
// Show the in-app notification
|
||||
} else if (userInteraction && !payload.userInfo?.local) {
|
||||
} else if (userInteraction && !payload.userInfo?.local) {
|
||||
// Swith to the server / team / channel that matches the notification
|
||||
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
if (componentId) {
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
if (componentId) {
|
||||
// Emit events to close the sidebars
|
||||
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NOTIFICATION_TYPE.SESSION:
|
||||
break;
|
||||
case NOTIFICATION_TYPE.SESSION:
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Session expired notification');
|
||||
console.log('Session expired notification');
|
||||
|
||||
// Logout the user from the server that matches the notification
|
||||
// Logout the user from the server that matches the notification
|
||||
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,13 +77,13 @@ export function componentDidAppearListener({componentId}: ComponentDidAppearEven
|
||||
EphemeralStore.addNavigationComponentId(componentId);
|
||||
|
||||
switch (componentId) {
|
||||
case 'MainSidebar':
|
||||
DeviceEventEmitter.emit(NavigationConstants.MAIN_SIDEBAR_DID_OPEN, this.handleSidebarDidOpen);
|
||||
DeviceEventEmitter.emit(NavigationConstants.BLUR_POST_DRAFT);
|
||||
break;
|
||||
case 'SettingsSidebar':
|
||||
DeviceEventEmitter.emit(NavigationConstants.BLUR_POST_DRAFT);
|
||||
break;
|
||||
case 'MainSidebar':
|
||||
DeviceEventEmitter.emit(NavigationConstants.MAIN_SIDEBAR_DID_OPEN, this.handleSidebarDidOpen);
|
||||
DeviceEventEmitter.emit(NavigationConstants.BLUR_POST_DRAFT);
|
||||
break;
|
||||
case 'SettingsSidebar':
|
||||
DeviceEventEmitter.emit(NavigationConstants.BLUR_POST_DRAFT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import {Notifications} from 'react-native-notifications';
|
||||
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
getDeliveredNotifications: Notifications.ios.getDeliveredNotifications,
|
||||
getPreferences: async () => null,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {gestureHandlerRootHOC} from 'react-native-gesture-handler';
|
||||
import {Screens} from '@constants';
|
||||
|
||||
// TODO: Remove this and uncomment screens as they get added
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable */
|
||||
|
||||
const withGestures = (screen: React.ComponentType<any>, styles: StyleProp<ViewStyle>) => {
|
||||
if (Platform.OS === 'android') {
|
||||
|
||||
@@ -23,6 +23,7 @@ module.exports = {
|
||||
'@assets': './dist/assets',
|
||||
'@components': './app/components',
|
||||
'@constants': './app/constants',
|
||||
'@database': './app/database',
|
||||
'@i18n': './app/i18n',
|
||||
'@init': './app/init',
|
||||
'@notifications': './app/notifications',
|
||||
|
||||
54
package-lock.json
generated
54
package-lock.json
generated
@@ -6319,6 +6319,13 @@
|
||||
"rambdax": "2.15.0",
|
||||
"rxjs": "^6.5.3",
|
||||
"sql-escape-string": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lokijs": {
|
||||
"version": "npm:@nozbe/lokijs@1.5.10-wmelon3",
|
||||
"resolved": "https://registry.npmjs.org/@nozbe/lokijs/-/lokijs-1.5.10-wmelon3.tgz",
|
||||
"integrity": "sha512-yfuj/SzYiVVn0e3OP8vjcbekumUR62Df90deG8uH7+5nqJqTLe4HkEzlmwJfss9UE0K8PsTQLACFOUq/2aAJ2A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@nozbe/with-observables": {
|
||||
@@ -6748,11 +6755,29 @@
|
||||
"eslint-visitor-keys": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"eslint-config-prettier": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz",
|
||||
"integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"get-stdin": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"eslint-plugin-jest": {
|
||||
"version": "22.4.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.4.1.tgz",
|
||||
"integrity": "sha512-gcLfn6P2PrFAVx3AobaOzlIEevpAEf9chTpFZz7bYfc7pz8XRv7vuKTIE4hxPKZSha6XWKKplDQ0x9Pq8xX2mg==",
|
||||
"dev": true
|
||||
},
|
||||
"eslint-plugin-prettier": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz",
|
||||
"integrity": "sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"prettier-linter-helpers": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -14894,15 +14919,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-config-prettier": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz",
|
||||
"integrity": "sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"get-stdin": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"eslint-plugin-eslint-comments": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz",
|
||||
@@ -15010,15 +15026,6 @@
|
||||
"from": "github:mattermost/eslint-plugin-mattermost#070ce792d105482ffb2b27cfc0b7e78b3d20acee",
|
||||
"dev": true
|
||||
},
|
||||
"eslint-plugin-prettier": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz",
|
||||
"integrity": "sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"prettier-linter-helpers": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"eslint-plugin-react": {
|
||||
"version": "7.21.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.21.5.tgz",
|
||||
@@ -23097,11 +23104,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"lokijs": {
|
||||
"version": "npm:@nozbe/lokijs@1.5.10-wmelon3",
|
||||
"resolved": "https://registry.npmjs.org/@nozbe/lokijs/-/lokijs-1.5.10-wmelon3.tgz",
|
||||
"integrity": "sha512-yfuj/SzYiVVn0e3OP8vjcbekumUR62Df90deG8uH7+5nqJqTLe4HkEzlmwJfss9UE0K8PsTQLACFOUq/2aAJ2A=="
|
||||
},
|
||||
"loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -25436,9 +25438,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"prettier": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz",
|
||||
"integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz",
|
||||
"integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==",
|
||||
"dev": true
|
||||
},
|
||||
"prettier-linter-helpers": {
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"@components/*": ["app/components/*"],
|
||||
"@constants": ["app/constants/index"],
|
||||
"@constants/*": ["app/constants/*"],
|
||||
"@database/*": ["app/database/*"],
|
||||
"@i18n": ["app/i18n/index"],
|
||||
"@init/*": ["app/init/*"],
|
||||
"@notifications": ["app/notifications/index"],
|
||||
|
||||
367
types/database/database.d.ts
vendored
367
types/database/database.d.ts
vendored
@@ -6,113 +6,336 @@ import Model from '@nozbe/watermelondb/Model';
|
||||
import {Migration} from '@nozbe/watermelondb/Schema/migrations';
|
||||
import {Class} from '@nozbe/watermelondb/utils/common';
|
||||
|
||||
import {IsolatedEntities, OperationType} from '../../app/database/admin/data_operator';
|
||||
import {DatabaseType} from '../../app/database/admin/database_manager';
|
||||
import {DatabaseType, IsolatedEntities} from './enums';
|
||||
|
||||
export type MigrationEvents = {
|
||||
onSuccess: () => void,
|
||||
onStarted: () => void,
|
||||
onFailure: (error: string) => void,
|
||||
}
|
||||
onSuccess: () => void;
|
||||
onStarted: () => void;
|
||||
onFailure: (error: string) => void;
|
||||
};
|
||||
|
||||
export type MMAdaptorOptions = {
|
||||
dbPath: string,
|
||||
schema: AppSchema,
|
||||
migrationSteps?: Migration [],
|
||||
migrationEvents?: MigrationEvents
|
||||
}
|
||||
dbPath: string;
|
||||
schema: AppSchema;
|
||||
migrationSteps?: Migration[];
|
||||
migrationEvents?: MigrationEvents;
|
||||
};
|
||||
|
||||
export type MMDatabaseConnection = {
|
||||
actionsEnabled?: boolean,
|
||||
dbName: string,
|
||||
dbType?: DatabaseType.DEFAULT | DatabaseType.SERVER,
|
||||
serverUrl?: string,
|
||||
}
|
||||
export type DatabaseConfigs = {
|
||||
actionsEnabled?: boolean;
|
||||
dbName: string;
|
||||
dbType?: DatabaseType.DEFAULT | DatabaseType.SERVER;
|
||||
serverUrl?: string;
|
||||
};
|
||||
|
||||
export type DefaultNewServer = {
|
||||
databaseFilePath: string,
|
||||
displayName: string,
|
||||
serverUrl: string
|
||||
}
|
||||
databaseFilePath: string;
|
||||
displayName: string;
|
||||
serverUrl: string;
|
||||
};
|
||||
|
||||
// A database connection is of type 'Database'; unless it fails to be initialize and in which case it becomes 'undefined'
|
||||
export type DBInstance = Database | undefined
|
||||
export type DatabaseInstance = Database | undefined;
|
||||
|
||||
export type RawApp = {
|
||||
buildNumber: string,
|
||||
createdAt: number,
|
||||
id: string,
|
||||
versionNumber: string,
|
||||
}
|
||||
buildNumber: string;
|
||||
createdAt: number;
|
||||
id: string;
|
||||
versionNumber: string;
|
||||
};
|
||||
|
||||
export type RawGlobal = {
|
||||
id: string,
|
||||
name: string,
|
||||
value: string,
|
||||
}
|
||||
id: string;
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type RawServers = {
|
||||
dbPath: string,
|
||||
displayName: string,
|
||||
id: string,
|
||||
mentionCount: number,
|
||||
unreadCount: number,
|
||||
url: string
|
||||
}
|
||||
dbPath: string;
|
||||
displayName: string;
|
||||
id: string;
|
||||
mentionCount: number;
|
||||
unreadCount: number;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type RawCustomEmoji = {
|
||||
id: string,
|
||||
name: string
|
||||
}
|
||||
id?: string;
|
||||
name: string;
|
||||
create_at?: number;
|
||||
update_at?: number;
|
||||
delete_at?: number;
|
||||
creator_id?: string;
|
||||
};
|
||||
|
||||
export type RawRole = {
|
||||
id: string,
|
||||
name: string,
|
||||
permissions: []
|
||||
}
|
||||
id: string;
|
||||
name: string;
|
||||
permissions: [];
|
||||
};
|
||||
|
||||
export type RawSystem = {
|
||||
id: string,
|
||||
name: string,
|
||||
value: string
|
||||
}
|
||||
id: string;
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type RawTermsOfService = {
|
||||
id: string,
|
||||
acceptedAt: number
|
||||
id: string;
|
||||
acceptedAt: number;
|
||||
};
|
||||
|
||||
export type RawDraft = {
|
||||
id?: string;
|
||||
channel_id: string;
|
||||
files?: FileInfo[];
|
||||
message?: string;
|
||||
root_id?: string;
|
||||
};
|
||||
|
||||
export type RawEmbed = { data: {}; type: string; url: string };
|
||||
|
||||
export type RawPostMetadata = {
|
||||
data: any;
|
||||
type: string;
|
||||
postId: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
interface PostMetadataTypes {
|
||||
embeds: PostEmbed;
|
||||
images: Dictionary<PostImage>;
|
||||
}
|
||||
|
||||
export type RecordValue = RawApp | RawGlobal | RawServers | RawCustomEmoji | RawRole | RawSystem | RawTermsOfService
|
||||
export type RawFile = {
|
||||
create_at: number;
|
||||
delete_at: number;
|
||||
extension: string;
|
||||
has_preview_image?: boolean;
|
||||
height: number;
|
||||
id?: string;
|
||||
localPath?: string;
|
||||
mime_type?: string;
|
||||
mini_preview?: string; // thumbnail
|
||||
name: string;
|
||||
post_id: string;
|
||||
size: number;
|
||||
update_at: number;
|
||||
user_id: string;
|
||||
width?: number;
|
||||
};
|
||||
|
||||
export type DataFactory = {
|
||||
db: Database,
|
||||
generator?: (model: Model) => void,
|
||||
optType?: OperationType,
|
||||
tableName?: string,
|
||||
value: RecordValue,
|
||||
export type RawReaction = {
|
||||
id?: string;
|
||||
create_at: number;
|
||||
delete_at: number;
|
||||
emoji_name: string;
|
||||
post_id: string;
|
||||
update_at: number;
|
||||
user_id: string;
|
||||
};
|
||||
|
||||
export type RawPostsInChannel = {
|
||||
id?: string;
|
||||
channel_id: string;
|
||||
earliest: number;
|
||||
latest: number;
|
||||
};
|
||||
|
||||
interface PostEmbed {
|
||||
type: PostEmbedType;
|
||||
url: string;
|
||||
data: Record<string, any>;
|
||||
}
|
||||
|
||||
export type Records = RecordValue | RecordValue[]
|
||||
|
||||
export type HandleBaseData = {
|
||||
optType: OperationType,
|
||||
tableName: string,
|
||||
values: Records,
|
||||
recordOperator: (recordOperator: DataFactory) => void
|
||||
interface PostImage {
|
||||
height: number;
|
||||
width: number;
|
||||
format?: string;
|
||||
frame_count?: number;
|
||||
}
|
||||
|
||||
export type BatchOperations = { db: Database, models: Model[] }
|
||||
interface PostImageMetadata extends PostImage {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type HandleIsolatedEntityData = { optType: OperationType, tableName: IsolatedEntities, values: Records }
|
||||
export type PostMetadataData = Record<string, any> | PostImageMetadata;
|
||||
|
||||
export type Models = Class<Model>[]
|
||||
export type PostMetadataType = 'images' | 'embeds';
|
||||
|
||||
// The RawPost describes the shape of the object received from a getPosts request
|
||||
export type RawPost = {
|
||||
channel_id: string;
|
||||
create_at: number;
|
||||
delete_at: number;
|
||||
edit_at: number;
|
||||
file_ids?: string[];
|
||||
filenames?: string[];
|
||||
hashtags: string;
|
||||
id: string;
|
||||
is_pinned?: boolean;
|
||||
last_reply_at?: number;
|
||||
message: string;
|
||||
original_id: string;
|
||||
parent_id: string;
|
||||
participants?: null;
|
||||
pending_post_id: string;
|
||||
prev_post_id?: string; // taken from getPosts API call; outside of post object
|
||||
props: object;
|
||||
reply_count?: number;
|
||||
root_id: string;
|
||||
type: string;
|
||||
update_at: number;
|
||||
user_id: string;
|
||||
metadata?: {
|
||||
embeds?: RawEmbed[];
|
||||
emojis?: RawCustomEmoji[];
|
||||
files?: RawFile[];
|
||||
images?: Dictionary<PostImage>;
|
||||
reactions?: RawReaction[];
|
||||
};
|
||||
};
|
||||
|
||||
export type RawChannelMembers = {
|
||||
channel_id: string;
|
||||
explicit_roles: string;
|
||||
last_update_at: number;
|
||||
last_viewed_at: number;
|
||||
mention_count: number;
|
||||
msg_count: number;
|
||||
notify_props: NotifyProps;
|
||||
roles: string;
|
||||
scheme_admin: boolean;
|
||||
scheme_guest: boolean;
|
||||
scheme_user: boolean;
|
||||
user_id: string;
|
||||
};
|
||||
|
||||
export type ChannelType = 'D' | 'O' | 'G' | 'P';
|
||||
|
||||
export type RawChannel = {
|
||||
create_at: number;
|
||||
creator_id: string;
|
||||
delete_at: number;
|
||||
display_name: string;
|
||||
extra_update_at: number;
|
||||
group_constrained: boolean | null;
|
||||
header: string;
|
||||
id: string;
|
||||
last_post_at: number;
|
||||
name: string;
|
||||
props: null;
|
||||
purpose: string;
|
||||
scheme_id: null;
|
||||
shared: null;
|
||||
team_id: string;
|
||||
total_msg_count: number;
|
||||
type: ChannelType;
|
||||
update_at: number;
|
||||
};
|
||||
|
||||
export type RawPostsInThread = {
|
||||
id?: string;
|
||||
earliest: number;
|
||||
latest?: number;
|
||||
post_id: string;
|
||||
};
|
||||
|
||||
export type RecordValue =
|
||||
| RawApp
|
||||
| RawCustomEmoji
|
||||
| RawDraft
|
||||
| RawFile
|
||||
| RawGlobal
|
||||
| RawPost
|
||||
| RawPostMetadata
|
||||
| RawPostsInChannel
|
||||
| RawPostsInThread
|
||||
| RawReaction
|
||||
| RawRole
|
||||
| RawServers
|
||||
| RawSystem
|
||||
| RawTermsOfService;
|
||||
|
||||
export type Operator = {
|
||||
database: Database;
|
||||
value: RecordValue;
|
||||
};
|
||||
|
||||
export type RecordOperator = (operator: Operator) => Promise<Model | null>;
|
||||
|
||||
export type BaseOperator = Operator & {
|
||||
generator: (model: Model) => void;
|
||||
tableName: string;
|
||||
};
|
||||
|
||||
export type ExecuteRecords = {
|
||||
tableName: string;
|
||||
values: RecordValue[];
|
||||
recordOperator: RecordOperator;
|
||||
};
|
||||
|
||||
export type PrepareRecords = ExecuteRecords & { database: Database };
|
||||
|
||||
export type BatchOperations = { database: Database; models: Model[] };
|
||||
|
||||
export type HandleIsolatedEntityData = {
|
||||
tableName: IsolatedEntities;
|
||||
values: RecordValue[];
|
||||
};
|
||||
|
||||
export type Models = Class<Model>[];
|
||||
|
||||
// The elements needed to create a new connection
|
||||
export type DatabaseConnection = {
|
||||
databaseConnection: MMDatabaseConnection,
|
||||
shouldAddToDefaultDatabase: boolean
|
||||
}
|
||||
configs: DatabaseConfigs;
|
||||
shouldAddToDefaultDatabase: boolean;
|
||||
};
|
||||
|
||||
// The elements required to switch to another active server database
|
||||
export type ActiveServerDatabase = { displayName: string, serverUrl: string }
|
||||
export type ActiveServerDatabase = { displayName: string; serverUrl: string };
|
||||
|
||||
export type HandleReactions = {
|
||||
reactions: RawReaction[];
|
||||
prepareRowsOnly: boolean;
|
||||
};
|
||||
|
||||
export type HandleFiles = {
|
||||
files: RawFile[];
|
||||
prepareRowsOnly: boolean;
|
||||
};
|
||||
|
||||
export type HandlePostMetadata = {
|
||||
embeds?: { embed: RawEmbed[]; postId: string }[];
|
||||
images?: { images: Dictionary<PostImage>; postId: string }[];
|
||||
prepareRowsOnly: boolean;
|
||||
};
|
||||
|
||||
export type HandlePosts = {
|
||||
orders: string[];
|
||||
values: RawPost[];
|
||||
previousPostId?: string;
|
||||
};
|
||||
|
||||
export type SanitizeReactions = {
|
||||
database: Database;
|
||||
post_id: string;
|
||||
rawReactions: RawReaction[];
|
||||
};
|
||||
|
||||
export type ChainPosts = {
|
||||
orders: string[];
|
||||
rawPosts: RawPost[];
|
||||
previousPostId: string;
|
||||
};
|
||||
|
||||
export type SanitizePosts = {
|
||||
posts: RawPost[];
|
||||
orders: string[];
|
||||
};
|
||||
|
||||
export type IdenticalRecord = {
|
||||
existingRecord: Model;
|
||||
newValue: RecordValue;
|
||||
tableName: string;
|
||||
};
|
||||
|
||||
24
types/database/enums.ts
Normal file
24
types/database/enums.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export enum OperationType {
|
||||
CREATE = 'CREATE',
|
||||
UPDATE = 'UPDATE',
|
||||
DELETE = 'DELETE',
|
||||
}
|
||||
|
||||
export enum IsolatedEntities {
|
||||
APP = 'app',
|
||||
GLOBAL = 'global',
|
||||
SERVERS = 'servers',
|
||||
CUSTOM_EMOJI = 'CustomEmoji',
|
||||
ROLE = 'Role',
|
||||
SYSTEM = 'System',
|
||||
TERMS_OF_SERVICE = 'TermsOfService',
|
||||
}
|
||||
|
||||
// The only two types of databases in the app
|
||||
export enum DatabaseType {
|
||||
DEFAULT,
|
||||
SERVER,
|
||||
}
|
||||
80
types/database/index.d.ts
vendored
80
types/database/index.d.ts
vendored
@@ -6,39 +6,23 @@ interface NotifyProps {
|
||||
desktop: string;
|
||||
desktop_sound: true;
|
||||
email: true;
|
||||
first_name: true
|
||||
first_name: true;
|
||||
mention_keys: string;
|
||||
push: string;
|
||||
}
|
||||
|
||||
interface UserProps {
|
||||
[userPropsName: string]: any
|
||||
[userPropsName: string]: any;
|
||||
}
|
||||
|
||||
interface Timezone {
|
||||
automaticTimezone: string
|
||||
manualTimezone: string,
|
||||
useAutomaticTimezone: true,
|
||||
}
|
||||
|
||||
interface PostEmbed {
|
||||
type: PostEmbedType;
|
||||
url: string;
|
||||
data: Record<string, any>;
|
||||
}
|
||||
|
||||
interface CustomEmoji {
|
||||
id: string;
|
||||
create_at: number;
|
||||
update_at: number;
|
||||
delete_at: number;
|
||||
creator_id: string;
|
||||
name: string;
|
||||
category: 'custom';
|
||||
automaticTimezone: string;
|
||||
manualTimezone: string;
|
||||
useAutomaticTimezone: true;
|
||||
}
|
||||
|
||||
interface FileInfo {
|
||||
id: string;
|
||||
id?: string;
|
||||
user_id: string;
|
||||
post_id: string;
|
||||
create_at: number;
|
||||
@@ -59,40 +43,18 @@ interface FileInfo {
|
||||
|
||||
type PostEmbedType = 'image' | 'message_attachment' | 'opengraph';
|
||||
|
||||
interface PostImage {
|
||||
height: number;
|
||||
width: number;
|
||||
format?: string;
|
||||
frame_count?: number;
|
||||
}
|
||||
|
||||
interface Reaction {
|
||||
user_id: string;
|
||||
post_id: string;
|
||||
emoji_name: string;
|
||||
create_at: number;
|
||||
}
|
||||
|
||||
interface PostMetadataTypes {
|
||||
embeds: Array<PostEmbed>;
|
||||
emojis: Array<CustomEmoji>;
|
||||
files: Array<FileInfo>;
|
||||
images: Dictionary<PostImage>;
|
||||
reactions: Array<Reaction>;
|
||||
}
|
||||
|
||||
type PostType = 'system_add_remove' |
|
||||
'system_add_to_channel' |
|
||||
'system_add_to_team' |
|
||||
'system_channel_deleted' |
|
||||
'system_channel_restored' |
|
||||
'system_displayname_change' |
|
||||
'system_convert_channel' |
|
||||
'system_ephemeral' |
|
||||
'system_header_change' |
|
||||
'system_join_channel' |
|
||||
'system_join_leave' |
|
||||
'system_leave_channel' |
|
||||
'system_purpose_change' |
|
||||
'system_remove_from_channel';
|
||||
|
||||
type PostType =
|
||||
| 'system_add_remove'
|
||||
| 'system_add_to_channel'
|
||||
| 'system_add_to_team'
|
||||
| 'system_channel_deleted'
|
||||
| 'system_channel_restored'
|
||||
| 'system_displayname_change'
|
||||
| 'system_convert_channel'
|
||||
| 'system_ephemeral'
|
||||
| 'system_header_change'
|
||||
| 'system_join_channel'
|
||||
| 'system_join_leave'
|
||||
| 'system_leave_channel'
|
||||
| 'system_purpose_change'
|
||||
| 'system_remove_from_channel';
|
||||
|
||||
5
types/database/post.d.ts
vendored
5
types/database/post.d.ts
vendored
@@ -31,6 +31,9 @@ export default class Post extends Model {
|
||||
/** delete_at : The timestamp to when this post was last archived/deleted */
|
||||
deleteAt: number;
|
||||
|
||||
/** update_at : The timestamp to when this post was last updated on the server */
|
||||
updateAt!: number;
|
||||
|
||||
/** edit_at : The timestamp to when this post was last edited */
|
||||
editAt: number;
|
||||
|
||||
@@ -59,7 +62,7 @@ export default class Post extends Model {
|
||||
userId: string;
|
||||
|
||||
/** props : Additional attributes for this props */
|
||||
props: string;
|
||||
props: object;
|
||||
|
||||
/** drafts : Every drafts associated with this Post */
|
||||
drafts: Draft;
|
||||
|
||||
3
types/database/post_metadata.d.ts
vendored
3
types/database/post_metadata.d.ts
vendored
@@ -3,6 +3,7 @@
|
||||
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
import {PostMetadataTypes} from '@typings/database/database';
|
||||
|
||||
import Post from '@typings/database/post';
|
||||
|
||||
@@ -23,7 +24,7 @@ export default class PostMetadata extends Model {
|
||||
type: string;
|
||||
|
||||
/** data : Different types of data ranging from arrays, emojis, files to images and reactions. */
|
||||
data: string;
|
||||
data: PostMetadataTypes;
|
||||
|
||||
/** post: The record representing the POST parent. */
|
||||
post: Relation<Post>;
|
||||
|
||||
Reference in New Issue
Block a user