[Gekidou] Refactor storage layer (#5471)

* Refactored storage layer - in progress

* Refactored DatabaseManager & Operators

* Renamed isRecordAppEqualToRaw to isRecordInfoEqualToRaw

* Review feedback

* Update app/database/models/app/info.ts

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>

* Update app/database/models/server/my_team.ts

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>

Co-authored-by: Avinash Lingaloo <>
Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
This commit is contained in:
Elias Nahum
2021-06-21 17:06:18 -04:00
committed by GitHub
parent 6f6d88f4d7
commit 17e832e689
156 changed files with 4125 additions and 5403 deletions

View File

@@ -4,10 +4,10 @@
import keyMirror from '@utils/key_mirror';
export const MM_TABLES = {
DEFAULT: {
APP: 'app',
GLOBAL: 'global',
SERVERS: 'servers',
APP: {
INFO: 'Info',
GLOBAL: 'Global',
SERVERS: 'Servers',
},
SERVER: {
CHANNEL: 'Channel',

View File

@@ -1,52 +1,37 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ComponentType, useEffect, useState} from 'react';
import React, {ComponentType, useState} from 'react';
import {Database} from '@nozbe/watermelondb';
import DatabaseProvider from '@nozbe/watermelondb/DatabaseProvider';
import {MM_TABLES} from '@constants/database';
import DatabaseManager from '@database/manager';
import Servers from '@database/models/default/servers';
import Servers from '@app/database/models/app/servers';
const {SERVERS} = MM_TABLES.DEFAULT;
//TODO: Remove when DatabaseManager is Singleton
const DBManager = new DatabaseManager();
const {SERVERS} = MM_TABLES.APP;
export function withServerDatabase<T>(
Component: ComponentType<T>,
): ComponentType<T> {
return function ServerDatabaseComponent(props) {
const [database, setDatabase] = useState<Database|unknown>();
const db = DatabaseManager.appDatabase?.database;
// If we don't need to await the async functions this side effect is not needed
useEffect(() => {
const observer = async (servers: Servers[]) => {
const server = servers.reduce((a, b) => (a.lastActiveAt > b.lastActiveAt ? a : b));
const observer = async (servers: Servers[]) => {
const server = servers.reduce((a, b) => (b.lastActiveAt > a.lastActiveAt ? b : a));
// The server database should already exists at this point
// there should not be a need to await
const serverDatabase = await DBManager.retrieveDatabaseInstances([server.url]);
if (serverDatabase?.length) {
setDatabase(serverDatabase[0]);
}
};
const serverDatabase = DatabaseManager.serverDatabases[server.url].database;
if (serverDatabase) {
setDatabase(serverDatabase);
}
};
const init = async () => {
// TODO: At this point the database should be already present
// there should not be a need to await
const db = await DBManager.getDefaultDatabase();
db?.collections.
get(SERVERS).
query().
observeWithColumns(['last_active_at']).
subscribe(observer);
};
init();
});
db?.collections.
get(SERVERS).
query().
observeWithColumns(['last_active_at']).
subscribe(observer);
if (!database) {
return null;

View File

@@ -4,60 +4,36 @@
import {Database, Q} from '@nozbe/watermelondb';
import LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs';
import {MM_TABLES} from '@constants/database';
import DefaultMigration from '@database/migration/default';
import {App, Global, Servers} from '@database/models/default';
import {defaultSchema} from '@database/schema/default';
import ServerMigration from '@database/migration/server';
import {
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,
} from '@database/models/server';
import {serverSchema} from '@database/schema/server';
import logger from '@nozbe/watermelondb/utils/common/logger';
import {
DatabaseConnectionArgs,
DatabaseInstance,
DatabaseInstances,
DefaultNewServerArgs,
GetDatabaseConnectionArgs,
Models,
RetrievedDatabase,
} from '@typings/database/database';
import {DatabaseType} from '@typings/database/enums';
import IGlobal from '@typings/database/global';
import IServers from '@typings/database/servers';
import {DeviceEventEmitter, Platform} from 'react-native';
import {FileSystem} from 'react-native-unimodules';
import urlParse from 'url-parse';
const {SERVERS, GLOBAL} = MM_TABLES.DEFAULT;
const DEFAULT_DATABASE = 'default';
const RECENTLY_VIEWED_SERVERS = 'RECENTLY_VIEWED_SERVERS';
import {MIGRATION_EVENTS, MM_TABLES} from '@constants/database';
import AppDataOperator from '@database/operator/app_data_operator';
import AppDatabaseMigrations from '@app/database/migration/app';
import {Info, Global, Servers} from '@app/database/models/app';
import {schema as appSchema} from '@app/database/schema/app';
import ServerDatabaseMigrations from '@database/migration/server';
import {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,
} from '@database/models/server';
import {serverSchema} from '@database/schema/server';
import {getActiveServer, getServer} from '@queries/app/servers';
import {deleteIOSDatabase} from '@utils/mattermost_managed';
import {hashCode} from '@utils/security';
import type {AppDatabase, CreateServerDatabaseArgs, MigrationEvents, Models, RegisterServerDatabaseArgs, ServerDatabase, ServerDatabases} from '@typings/database/database';
import {DatabaseType} from '@typings/database/enums';
import type IServers from '@typings/database/models/app/servers';
import ServerDataOperator from '../../operator/server_data_operator';
const {SERVERS} = MM_TABLES.APP;
const APP_DATABASE = 'app';
if (__DEV__) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -66,367 +42,283 @@ if (__DEV__) {
}
class DatabaseManager {
private defaultDatabase: DatabaseInstance;
private readonly defaultModels: Models;
private readonly iOSAppGroupDatabase: string | null;
private readonly androidFilesDirectory: string | null;
private readonly serverModels: Models;
public appDatabase?: AppDatabase;
public serverDatabases: ServerDatabases = {};
private readonly appModels: Models;
private readonly databaseDirectory: string | null;
private readonly serverModels: Models;
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.iOSAppGroupDatabase = null;
this.androidFilesDirectory = null;
}
constructor() {
this.appModels = [Info, 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.databaseDirectory = '';
}
/**
* getDatabaseConnection: Given a server url (serverUrl) and a flag (setAsActiveDatabase), this method will attempt
* to retrieve an existing database connection previously created for that url. If not found, it will create a new connection and register it in the DEFAULT_DATABASE
* @param {GetDatabaseConnectionArgs} getDatabaseConnection
* @param {string} getDatabaseConnection.connectionName
* @param {string} getDatabaseConnection.serverUrl
* @param {boolean} getDatabaseConnection.setAsActiveDatabase
* @returns {Promise<DatabaseInstance>}
*/
getDatabaseConnection = async ({
serverUrl,
setAsActiveDatabase,
connectionName,
}: GetDatabaseConnectionArgs) => {
// We potentially already have this server registered; so we'll try to retrieve it if it is present under DEFAULT_DATABASE/GLOBAL entity
const existingServers = (await this.retrieveDatabaseInstances([
serverUrl,
])) as RetrievedDatabase[];
public init = async (serverUrls: string[]): Promise<void> => {
await this.createAppDatabase();
for await (const serverUrl of serverUrls) {
await this.initServerDatabase(serverUrl);
}
};
// Since we only passed one serverUrl, we'll expect only one value in the array
const serverDatabase = existingServers?.[0];
private createAppDatabase = async (): Promise<AppDatabase|undefined> => {
try {
const modelClasses = this.appModels;
const schema = appSchema;
let connection: DatabaseInstance;
const adapter = new LokiJSAdapter({dbName: APP_DATABASE, migrations: AppDatabaseMigrations, schema, useWebWorker: false, useIncrementalIndexedDB: true});
if (serverDatabase) {
// This serverUrl has previously been registered on the app
connection = serverDatabase.dbInstance;
} else {
// Or, it might be that the user has this server on the web-app but not mobile-app; so we'll need to create a new entry for this new serverUrl
const databaseName = connectionName ?? urlParse(serverUrl).hostname;
connection = await this.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
dbName: databaseName,
dbType: DatabaseType.SERVER,
serverUrl,
},
});
}
const database = new Database({adapter, actionsEnabled: true, modelClasses});
const operator = new AppDataOperator(database);
if (setAsActiveDatabase) {
await this.setActiveServerDatabase(serverUrl);
}
this.appDatabase = {
database,
operator,
};
return connection;
};
return this.appDatabase;
} catch (e) {
// TODO : report to sentry? Show something on the UI ?
}
/**
* 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 {DatabaseConfigs} databaseConnection
* @param {boolean} shouldAddToDefaultDatabase
*
* @returns {Promise<DatabaseInstance>}
*/
createDatabaseConnection = async ({configs, shouldAddToDefaultDatabase = true}: DatabaseConnectionArgs): Promise<DatabaseInstance> => {
const {actionsEnabled = true, dbName = DEFAULT_DATABASE, dbType = DatabaseType.DEFAULT, serverUrl = undefined} = configs;
return undefined;
};
try {
const databaseName = dbType === DatabaseType.DEFAULT ? DEFAULT_DATABASE : dbName;
public createServerDatabase = async ({config}: CreateServerDatabaseArgs): Promise<ServerDatabase|undefined> => {
const {dbName, displayName, serverUrl} = config;
// const databaseFilePath = this.getDatabaseFilePath(databaseName);
const migrations = dbType === DatabaseType.DEFAULT ? DefaultMigration : ServerMigration;
const modelClasses = dbType === DatabaseType.DEFAULT ? this.defaultModels : this.serverModels;
const schema = dbType === DatabaseType.DEFAULT ? defaultSchema : serverSchema;
if (serverUrl) {
try {
const databaseFilePath = this.getDatabaseFilePath(dbName);
const migrations = ServerDatabaseMigrations;
const modelClasses = this.serverModels;
const schema = serverSchema;
const adapter = new LokiJSAdapter({dbName: databaseName, migrations, schema, useWebWorker: false, useIncrementalIndexedDB: true});
const adapter = new LokiJSAdapter({dbName, migrations, schema, useWebWorker: false, useIncrementalIndexedDB: true});
// Registers the new server connection into the DEFAULT database
if (serverUrl && shouldAddToDefaultDatabase) {
await this.addServerToDefaultDatabase({
databaseFilePath: databaseName,
displayName: dbName,
serverUrl,
});
}
return new Database({adapter, actionsEnabled, modelClasses});
} catch (e) {
// eslint-disable-next-line no-console
console.log('createDatabaseConnection ERROR:', e);
}
// Registers the new server connection into the DEFAULT database
await this.addServerToAppDatabase({
databaseFilePath,
displayName: displayName || dbName,
serverUrl,
});
return undefined;
};
const database = new Database({adapter, actionsEnabled: true, modelClasses});
const operator = new ServerDataOperator(database);
const serverDatabase = {database, operator};
/**
* setActiveServerDatabase: Set the new active server database. The serverUrl is used to ensure that we do not duplicate entries in the default database.
* This method should be called when switching to another server.
* @param {string} serverUrl
* @returns {Promise<void>}
*/
setActiveServerDatabase = async (serverUrl: string) => {
const defaultDatabase = await this.getDefaultDatabase();
this.serverDatabases[serverUrl] = serverDatabase;
if (defaultDatabase) {
// retrieve recentlyViewedServers from Global entity
const recentlyViewedServers = (await defaultDatabase.collections.get(GLOBAL).query(Q.where('name', RECENTLY_VIEWED_SERVERS)).fetch()) as IGlobal[];
return serverDatabase;
} catch (e) {
// TODO : report to sentry? Show something on the UI ?
}
}
if (recentlyViewedServers.length) {
// We have previously written something for this flag
const flagRecord = recentlyViewedServers[0];
const recentList = Array.from(flagRecord.value);
recentList.unshift(serverUrl);
return undefined;
};
// so as to avoid duplicate in this array
const sanitizedList = Array.from(new Set(recentList));
private initServerDatabase = async (serverUrl: string): Promise<void> => {
await this.createServerDatabase({
config: {
dbName: hashCode(serverUrl),
dbType: DatabaseType.SERVER,
serverUrl,
},
});
};
await defaultDatabase.action(async () => {
await flagRecord.update((record) => {
record.value = sanitizedList;
});
});
} else {
// The flag hasn't been set; so we create the record
await defaultDatabase.action(async () => {
await defaultDatabase.collections.get(GLOBAL).create((record: IGlobal) => {
record.name = RECENTLY_VIEWED_SERVERS;
record.value = [serverUrl];
});
});
}
}
};
private addServerToAppDatabase = async ({databaseFilePath, displayName, serverUrl}: RegisterServerDatabaseArgs): Promise<void> => {
try {
const isServerPresent = await this.isServerPresent(serverUrl); // TODO: Use normalized serverUrl
/**
* isServerPresent : Confirms if the current serverUrl does not already exist in the database
* @param {String} serverUrl
* @returns {Promise<boolean>}
*/
isServerPresent = async (serverUrl: string) => {
const allServers = await this.getAllServers([serverUrl]);
return allServers?.length > 0;
};
if (this.appDatabase?.database && !isServerPresent) {
const appDatabase = this.appDatabase.database;
await appDatabase.action(async () => {
const serversCollection = appDatabase.collections.get(SERVERS);
await serversCollection.create((server: IServers) => {
server.dbPath = databaseFilePath;
server.displayName = displayName;
server.mentionCount = 0;
server.unreadCount = 0;
server.url = serverUrl; // TODO: Use normalized serverUrl
server.isSecured = urlParse(serverUrl).protocol === 'https';
server.lastActiveAt = 0;
});
});
}
} catch (e) {
// TODO : report to sentry? Show something on the UI ?
}
};
/**
* getActiveServerUrl: Use this getter method to retrieve the active server URL.
* @returns {string}
*/
getActiveServerUrl = async (): Promise<string|undefined> => {
const defaultDatabase = await this.getDefaultDatabase();
private isServerPresent = async (serverUrl: string): Promise<boolean> => {
if (this.appDatabase?.database) {
const server = await getServer(this.appDatabase.database, serverUrl);
return Boolean(server);
}
if (defaultDatabase) {
const serverRecords = await defaultDatabase.collections.get(GLOBAL).query(Q.where('name', RECENTLY_VIEWED_SERVERS)).fetch() as IGlobal[];
return false;
}
if (serverRecords.length) {
const recentServers = serverRecords[0].value as string[];
return recentServers[0];
}
return undefined;
}
return undefined;
};
public getActiveServerUrl = async (): Promise<string|null|undefined> => {
const database = this.appDatabase?.database;
if (database) {
const server = await getActiveServer(database);
return server?.url;
}
/**
* 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 {Promise<DatabaseInstance | undefined>}
*/
getActiveServerDatabase = async (): Promise<DatabaseInstance> => {
const serverUrl = await this.getActiveServerUrl();
return null;
}
if (serverUrl) {
const serverDatabase = await this.getDatabaseConnection({serverUrl, setAsActiveDatabase: false});
return serverDatabase;
}
return undefined;
};
public getActiveServerDatabase = async (): Promise<Database|undefined> => {
const database = this.appDatabase?.database;
if (database) {
const server = await getActiveServer(database);
if (server?.url) {
return this.serverDatabases[server.url].database;
}
}
/**
* getDefaultDatabase : Returns the default database.
* @returns {Database} default database
*/
getDefaultDatabase = async (): Promise<DatabaseInstance> => {
if (!this.defaultDatabase) {
await this.setDefaultDatabase();
}
return this.defaultDatabase;
};
return undefined;
}
/**
* retrieveDatabaseInstances: Using an array of server URLs, this method creates a database connection for each URL
* and return them to the caller.
*
* @param {string[]} serverUrls
* @returns {Promise<RetrievedDatabase[] | null>}
*/
retrieveDatabaseInstances = async (serverUrls: string[]): Promise<DatabaseInstances[] | null> => {
// Retrieve all server records from the default db
const allServers = await this.getAllServers(serverUrls);
public setActiveServerDatabase = async (serverUrl: string): Promise<void> => {
if (this.appDatabase?.database) {
const database = this.appDatabase?.database;
await database.action(async () => {
const servers = await database.collections.get(SERVERS).query(Q.where('url', serverUrl)).fetch();
if (servers.length) {
servers[0].update((server: Servers) => {
server.lastActiveAt = Date.now();
});
}
});
}
};
// Creates server database instances
if (allServers.length) {
const databasePromises = allServers.map(async (server: IServers) => {
const {displayName, url} = server;
public deleteServerDatabase = async (serverUrl: string): Promise<void> => {
if (this.appDatabase?.database) {
const database = this.appDatabase?.database;
const server = await getServer(database, serverUrl);
if (server) {
database.action(() => {
server.update((record) => {
record.lastActiveAt = 0;
});
});
// Since we are retrieving existing URL ( and so database connections ) from the 'DEFAULT' database, shouldAddToDefaultDatabase is set to false
const dbInstance = await this.createDatabaseConnection({
configs: {
actionsEnabled: true,
dbName: displayName,
dbType: DatabaseType.SERVER,
serverUrl: url,
},
shouldAddToDefaultDatabase: false,
});
return {url, dbInstance, displayName};
});
const databaseInstances = await Promise.all(databasePromises);
return databaseInstances;
}
return null;
};
delete this.serverDatabases[serverUrl];
this.deleteServerDatabaseFiles(serverUrl);
}
}
}
/**
* deleteDatabase: Removes the *.db file from the App-Group directory for iOS or the files directory on Android.
* Also, it removes its entry in the 'servers' table from the DEFAULT database
* @param {string} serverUrl
* @returns {Promise<boolean>}
*/
deleteDatabase = async (serverUrl: string): Promise<boolean> => {
try {
const defaultDatabase = await this.getDefaultDatabase();
let server: IServers;
let result = true;
if (defaultDatabase) {
const serversRecords = (await defaultDatabase.collections.get(SERVERS).query(Q.where('url', serverUrl)).fetch()) as IServers[];
server = serversRecords?.[0] ?? undefined;
public destroyServerDatabase = async (serverUrl: string): Promise<void> => {
if (this.appDatabase?.database) {
const database = this.appDatabase?.database;
const server = await getServer(database, serverUrl);
if (server) {
database.action(async () => {
await server.destroyPermanently();
});
const globalRecords = await defaultDatabase.collections.get(GLOBAL).query(Q.where('name', RECENTLY_VIEWED_SERVERS)).fetch() as IGlobal[];
const global = globalRecords?.[0] ?? undefined;
delete this.serverDatabases[serverUrl];
this.deleteServerDatabaseFiles(serverUrl);
}
}
}
if (server) {
// Perform a delete operation for this server record on the 'servers' table in default database
await defaultDatabase.action(async () => {
await server.destroyPermanently();
});
private deleteServerDatabaseFiles = async (serverUrl: string): Promise<void> => {
const databaseName = hashCode(serverUrl);
result = result && true;
}
if (Platform.OS === 'ios') {
// On iOS, we'll delete the *.db file under the shared app-group/databases folder
deleteIOSDatabase({databaseName});
return;
}
if (global) {
// filter out the deleted serverURL
const urls = global.value as string[];
const filtered = urls.filter((url) => url !== serverUrl);
await defaultDatabase.action(async () => {
await global.update((record) => {
record.value = filtered;
});
});
result = result && true;
}
return result;
}
return false;
} catch (e) {
return false;
}
};
// On Android, we'll delete both the *.db file and the *.db-journal file
const androidFilesDir = `${this.databaseDirectory}databases/`;
const databaseFile = `${androidFilesDir}${databaseName}.db`;
const databaseJournal = `${androidFilesDir}${databaseName}.db-journal`;
/**
* getAllServers : Retrieves all the servers registered in the default database
* @returns {Promise<undefined | Servers[]>}
*/
private getAllServers = async (serverUrls: string[]) => {
// Retrieve all server records from the default db
const defaultDatabase = await this.getDefaultDatabase();
if (defaultDatabase) {
const allServers = (await defaultDatabase.collections.
get(SERVERS).
query(Q.where('url', Q.oneOf(serverUrls))).
fetch()) as IServers[];
return allServers;
}
return [];
};
await FileSystem.deleteAsync(databaseFile);
await FileSystem.deleteAsync(databaseJournal);
}
/**
* setDefaultDatabase : Sets the default database.
* @returns {Promise<DatabaseInstance>}
*/
private setDefaultDatabase = async (): Promise<DatabaseInstance> => {
this.defaultDatabase = await this.createDatabaseConnection({
configs: {dbName: DEFAULT_DATABASE},
shouldAddToDefaultDatabase: false,
});
factoryReset = async (shouldRemoveDirectory: boolean): Promise<boolean> => {
try {
//On iOS, we'll delete the databases folder under the shared AppGroup folder
if (Platform.OS === 'ios') {
deleteIOSDatabase({shouldRemoveDirectory});
return true;
}
return this.defaultDatabase;
};
// On Android, we'll remove the databases folder under the Document Directory
const androidFilesDir = `${this.databaseDirectory}databases/`;
await FileSystem.deleteAsync(androidFilesDir);
return true;
} catch (e) {
return false;
}
};
/**
* addServerToDefaultDatabase: Adds a record into the 'default' database - into the 'servers' table - for this new server connection
* @param {string} databaseFilePath
* @param {string} displayName
* @param {string} serverUrl
* @returns {Promise<void>}
*/
private addServerToDefaultDatabase = async ({databaseFilePath, displayName, serverUrl}: DefaultNewServerArgs) => {
try {
const defaultDatabase = await this.getDefaultDatabase();
const isServerPresent = await this.isServerPresent(serverUrl);
private buildMigrationCallbacks = (dbName: string) => {
const migrationEvents: MigrationEvents = {
onSuccess: () => {
return DeviceEventEmitter.emit(MIGRATION_EVENTS.MIGRATION_SUCCESS, {
dbName,
});
},
onStarted: () => {
return DeviceEventEmitter.emit(MIGRATION_EVENTS.MIGRATION_STARTED, {
dbName,
});
},
onFailure: (error) => {
return DeviceEventEmitter.emit(MIGRATION_EVENTS.MIGRATION_ERROR, {
dbName,
error,
});
},
};
if (defaultDatabase && !isServerPresent) {
await defaultDatabase.action(async () => {
const serversCollection = defaultDatabase.collections.get(SERVERS);
await serversCollection.create((server: IServers) => {
server.dbPath = databaseFilePath;
server.displayName = displayName;
server.mentionCount = 0;
server.unreadCount = 0;
server.url = serverUrl;
});
});
}
} catch (e) {
// eslint-disable-next-line no-console
console.log('addServerToDefaultDatabase ERROR:', e);
}
};
return migrationEvents;
};
private getDatabaseFilePath = (dbName: string): string => {
return Platform.OS === 'ios' ? `${this.databaseDirectory}/${dbName}.db` : `${this.databaseDirectory}${dbName}.db`;
};
}
export default DatabaseManager;
export default new DatabaseManager();

View File

@@ -6,187 +6,95 @@ 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 DefaultMigration from '@database/migration/default';
import {App, Global, Servers} from '@database/models/default';
import {defaultSchema} from '@database/schema/default';
import ServerMigration from '@database/migration/server';
import {
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,
} from '@database/models/server';
import {serverSchema} from '@database/schema/server';
import {
DatabaseConnectionArgs,
DatabaseInstance,
DatabaseInstances,
DefaultNewServerArgs,
GetDatabaseConnectionArgs,
MigrationEvents,
Models,
RetrievedDatabase,
} from '@typings/database/database';
import {DatabaseType} from '@typings/database/enums';
import IServers from '@typings/database/servers';
import IGlobal from '@typings/database/global';
import {deleteIOSDatabase, getIOSAppGroupDetails} from '@utils/mattermost_managed';
import urlParse from 'url-parse';
const {SERVERS, GLOBAL} = MM_TABLES.DEFAULT;
const DEFAULT_DATABASE = 'default';
const RECENTLY_VIEWED_SERVERS = 'RECENTLY_VIEWED_SERVERS';
import {MIGRATION_EVENTS, MM_TABLES} from '@constants/database';
import AppDataOperator from '@database/operator/app_data_operator';
import AppDatabaseMigrations from '@app/database/migration/app';
import {Info, Global, Servers} from '@app/database/models/app';
import {schema as appSchema} from '@app/database/schema/app';
import ServerDatabaseMigrations from '@database/migration/server';
import {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,
} from '@database/models/server';
import {serverSchema} from '@database/schema/server';
import {getActiveServer, getServer} from '@queries/app/servers';
import {deleteIOSDatabase, getIOSAppGroupDetails} from '@utils/mattermost_managed';
import {hashCode} from '@utils/security';
import type {AppDatabase, CreateServerDatabaseArgs, RegisterServerDatabaseArgs, MigrationEvents, Models, ServerDatabase, ServerDatabases} from '@typings/database/database';
import {DatabaseType} from '@typings/database/enums';
import type IServers from '@typings/database/models/app/servers';
import ServerDataOperator from '../operator/server_data_operator';
const {SERVERS} = MM_TABLES.APP;
const APP_DATABASE = 'app';
class DatabaseManager {
private defaultDatabase: DatabaseInstance;
private readonly defaultModels: Models;
private readonly iOSAppGroupDatabase: string | null;
private readonly androidFilesDirectory: string | null;
public appDatabase?: AppDatabase;
public serverDatabases: ServerDatabases = {};
private readonly appModels: Models;
private readonly databaseDirectory: string | null;
private readonly serverModels: Models;
constructor() {
this.defaultModels = [App, Global, Servers];
this.appModels = [Info, 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,
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;
this.databaseDirectory = Platform.OS === 'ios' ? getIOSAppGroupDetails().appGroupDatabase : FileSystem.documentDirectory;
}
/**
* getDatabaseConnection: Given a server url (serverUrl) and a flag (setAsActiveDatabase), this method will attempt
* to retrieve an existing database connection previously created for that url.
* If not found, it will create a new connection and register it in the DEFAULT_DATABASE. In this case, it will also
* use the provided connectionName as display_name for this server
* @param {GetDatabaseConnectionArgs} getDatabaseConnection
* @param {string} getDatabaseConnection.connectionName
* @param {string} getDatabaseConnection.serverUrl
* @param {boolean} getDatabaseConnection.setAsActiveDatabase
* @returns {Promise<DatabaseInstance>}
*/
getDatabaseConnection = async ({serverUrl, setAsActiveDatabase, connectionName}: GetDatabaseConnectionArgs) => {
// We potentially already have this server registered; so we'll try to retrieve it if it is present under DEFAULT_DATABASE/GLOBAL entity
const existingServers = await this.retrieveDatabaseInstances([serverUrl]) as RetrievedDatabase[];
// Since we only passed one serverUrl, we'll expect only one value in the array
const serverDatabase = existingServers?.[0];
let connection: DatabaseInstance;
if (serverDatabase) {
// This serverUrl has previously been registered on the app
connection = serverDatabase.dbInstance;
} else {
// Or, it might be that the user has this server on the web-app but not mobile-app; so we'll need to create a new entry for this new serverUrl
const databaseName = connectionName ?? urlParse(serverUrl).hostname;
connection = await this.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
dbName: databaseName,
dbType: DatabaseType.SERVER,
serverUrl,
},
});
* init : Retrieves all the servers registered in the default database
* @param {string[]} serverUrls
* @returns {Promise<void>}
*/
public init = async (serverUrls: string[]): Promise<void> => {
await this.createAppDatabase();
for await (const serverUrl of serverUrls) {
await this.initServerDatabase(serverUrl);
}
if (setAsActiveDatabase) {
await this.setActiveServerDatabase(serverUrl);
}
return connection;
};
/**
* 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 {boolean} shouldAddToDefaultDatabase
*
* @returns {Promise<DatabaseInstance>}
* createAppDatabase: Creates the App database. However,
* if a database could not be created, it will return undefined.
* @returns {Promise<AppDatabase|undefined>}
*/
createDatabaseConnection = async ({configs, shouldAddToDefaultDatabase = true}: DatabaseConnectionArgs): Promise<DatabaseInstance> => {
const {actionsEnabled = true, dbName = DEFAULT_DATABASE, dbType = DatabaseType.DEFAULT, serverUrl = undefined} = configs;
private createAppDatabase = async (): Promise<AppDatabase|undefined> => {
try {
const databaseName = dbType === DatabaseType.DEFAULT ? DEFAULT_DATABASE : dbName;
const databaseName = APP_DATABASE;
const databaseFilePath = this.getDatabaseFilePath(databaseName);
const migrations = dbType === DatabaseType.DEFAULT ? DefaultMigration : ServerMigration;
const modelClasses = dbType === DatabaseType.DEFAULT ? this.defaultModels : this.serverModels;
const schema = dbType === DatabaseType.DEFAULT ? defaultSchema : serverSchema;
const modelClasses = this.appModels;
const schema = appSchema;
const adapter = new SQLiteAdapter({
dbName: databaseFilePath,
migrationEvents: this.buildMigrationCallbacks(databaseName),
migrations,
migrations: AppDatabaseMigrations,
schema,
});
// Registers the new server connection into the DEFAULT database
if (serverUrl && shouldAddToDefaultDatabase) {
await this.addServerToDefaultDatabase({
databaseFilePath,
displayName: dbName,
serverUrl,
});
}
const database = new Database({adapter, actionsEnabled: true, modelClasses});
const operator = new AppDataOperator(database);
return new Database({adapter, actionsEnabled, modelClasses});
this.appDatabase = {
database,
operator,
};
return this.appDatabase;
} catch (e) {
// TODO : report to sentry? Show something on the UI ?
}
@@ -195,186 +103,228 @@ class DatabaseManager {
};
/**
* setActiveServerDatabase: Set the new active server database. The serverUrl is used to ensure that we do not duplicate entries in the default database.
* This method should be called when switching to another server.
* @param {string} serverUrl
* @returns {Promise<void>}
* createServerDatabase: Creates a server database and registers the the server in the app database. However,
* if a database connection could not be created, it will return undefined.
* @param {CreateServerDatabaseArgs} createServerDatabaseArgs
*
* @returns {Promise<ServerDatabase|undefined>}
*/
setActiveServerDatabase = async (serverUrl: string) => {
const defaultDatabase = await this.getDefaultDatabase();
if (defaultDatabase) {
// retrieve recentlyViewedServers from Global entity
const recentlyViewedServers = await defaultDatabase.collections.get(GLOBAL).query(Q.where('name', RECENTLY_VIEWED_SERVERS)).fetch() as IGlobal[];
public createServerDatabase = async ({config}: CreateServerDatabaseArgs): Promise<ServerDatabase|undefined> => {
const {dbName, displayName, serverUrl} = config;
if (recentlyViewedServers.length) {
// We have previously written something for this flag
const flagRecord = recentlyViewedServers[0];
const recentList = Array.from(flagRecord.value);
recentList.unshift(serverUrl);
if (serverUrl) {
try {
const databaseName = hashCode(dbName);
const databaseFilePath = this.getDatabaseFilePath(databaseName);
const migrations = ServerDatabaseMigrations;
const modelClasses = this.serverModels;
const schema = serverSchema;
// so as to avoid duplicate in this array
const sanitizedList = Array.from(new Set(recentList));
const adapter = new SQLiteAdapter({
dbName: databaseFilePath,
migrationEvents: this.buildMigrationCallbacks(databaseName),
migrations,
schema,
});
await defaultDatabase.action(async () => {
await flagRecord.update((record) => {
record.value = sanitizedList;
});
});
} else {
// The flag hasn't been set; so we create the record
await defaultDatabase.action(async () => {
await defaultDatabase.collections.get(GLOBAL).create((record: IGlobal) => {
record.name = RECENTLY_VIEWED_SERVERS;
record.value = [serverUrl];
});
});
}
}
};
// Registers the new server connection into the DEFAULT database
await this.addServerToAppDatabase({
databaseFilePath,
displayName: displayName || dbName,
serverUrl,
});
/**
* getActiveServerUrl: Use this getter method to retrieve the active server URL.
* @returns {string | undefined}
*/
getActiveServerUrl = async (): Promise<string | undefined> => {
const defaultDatabase = await this.getDefaultDatabase();
const database = new Database({adapter, actionsEnabled: true, modelClasses});
const operator = new ServerDataOperator(database);
const serverDatabase = {database, operator};
if (defaultDatabase) {
const serverRecords = await defaultDatabase.collections.get(GLOBAL).query(Q.where('name', RECENTLY_VIEWED_SERVERS)).fetch() as IGlobal[];
this.serverDatabases[serverUrl] = serverDatabase;
if (serverRecords.length) {
const recentServers = serverRecords[0].value as string[];
return recentServers[0];
return serverDatabase;
} catch (e) {
// TODO : report to sentry? Show something on the UI ?
}
return undefined;
}
return undefined;
};
/**
* 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 {Promise<MostRecentConnection | undefined>}
*/
getActiveServerDatabase = async (): Promise<DatabaseInstance> => {
const serverUrl = await this.getActiveServerUrl();
if (serverUrl) {
const serverDatabase = await this.getDatabaseConnection({serverUrl, setAsActiveDatabase: false});
return serverDatabase;
}
return undefined;
* initServerDatabase : initializes the server database.
* @param {string} serverUrl
* @returns {Promise<void>}
*/
private initServerDatabase = async (serverUrl: string): Promise<void> => {
await this.createServerDatabase({
config: {
dbName: serverUrl,
dbType: DatabaseType.SERVER,
serverUrl,
},
});
};
/**
* getDefaultDatabase : Returns the default database.
* @returns {Database} default database
*/
getDefaultDatabase = async (): Promise<DatabaseInstance> => {
if (!this.defaultDatabase) {
await this.setDefaultDatabase();
}
return this.defaultDatabase;
};
* addServerToAppDatabase: Adds a record in the 'app' database - into the 'servers' table - for this new server connection
* @param {string} databaseFilePath
* @param {string} displayName
* @param {string} serverUrl
* @returns {Promise<void>}
*/
private addServerToAppDatabase = async ({databaseFilePath, displayName, serverUrl}: RegisterServerDatabaseArgs): Promise<void> => {
try {
const isServerPresent = await this.isServerPresent(serverUrl); // TODO: Use normalized serverUrl
/**
* retrieveDatabaseInstances: Using an array of server URLs, this method creates a database connection for each URL
* and return them to the caller.
*
* @param {string[]} serverUrls
* @returns {Promise<RetrievedDatabase[] | null>}
*/
retrieveDatabaseInstances = async (serverUrls: string[]): Promise<DatabaseInstances[] | null> => {
// Retrieve all server records from the default db
const allServers = await this.getAllServers(serverUrls);
// Creates server database instances
if (allServers.length) {
const databasePromises = await allServers.map(async (server: IServers) => {
const {displayName, url} = server;
// Since we are retrieving existing URL ( and so database connections ) from the 'DEFAULT' database, shouldAddToDefaultDatabase is set to false
const dbInstance = await this.createDatabaseConnection({
configs: {
actionsEnabled: true,
dbName: displayName,
dbType: DatabaseType.SERVER,
serverUrl: url,
},
shouldAddToDefaultDatabase: false,
if (this.appDatabase?.database && !isServerPresent) {
const appDatabase = this.appDatabase.database;
await appDatabase.action(async () => {
const serversCollection = appDatabase.collections.get(SERVERS);
await serversCollection.create((server: IServers) => {
server.dbPath = databaseFilePath;
server.displayName = displayName;
server.mentionCount = 0;
server.unreadCount = 0;
server.url = serverUrl; // TODO: Use normalized serverUrl
server.isSecured = urlParse(serverUrl).protocol === 'https';
server.lastActiveAt = 0;
});
});
return {dbInstance, displayName, url};
});
const databaseInstances = await Promise.all(databasePromises);
return databaseInstances;
}
} catch (e) {
// TODO : report to sentry? Show something on the UI ?
}
return null;
};
/**
* deleteDatabase: Removes the *.db file from the App-Group directory for iOS or the files directory on Android.
* Also, it removes its entry in the 'servers' table from the DEFAULT database
* isServerPresent : Confirms if the current serverUrl does not already exist in the database
* @param {String} serverUrl
* @returns {Promise<boolean>}
*/
private isServerPresent = async (serverUrl: string): Promise<boolean> => {
if (this.appDatabase?.database) {
const server = await getServer(this.appDatabase.database, serverUrl);
return Boolean(server);
}
return false;
}
/**
* getActiveServerUrl: Get the record for active server database.
* @returns {Promise<string|null|undefined>}
*/
public getActiveServerUrl = async (): Promise<string|null|undefined> => {
const database = this.appDatabase?.database;
if (database) {
const server = await getActiveServer(database);
return server?.url;
}
return null;
}
/**
* getActiveServerDatabase: Get the record for active server database.
* @returns {Promise<Database|undefined>}
*/
public getActiveServerDatabase = async (): Promise<Database|undefined> => {
const database = this.appDatabase?.database;
if (database) {
const server = await getActiveServer(database);
if (server?.url) {
return this.serverDatabases[server.url].database;
}
}
return undefined;
}
/**
* setActiveServerDatabase: Set the new active server database.
* This method should be called when switching to another server.
* @param {string} serverUrl
* @returns {Promise<void>}
*/
public setActiveServerDatabase = async (serverUrl: string): Promise<void> => {
if (this.appDatabase?.database) {
const database = this.appDatabase?.database;
await database.action(async () => {
const servers = await database.collections.get(SERVERS).query(Q.where('url', serverUrl)).fetch();
if (servers.length) {
servers[0].update((server: Servers) => {
server.lastActiveAt = Date.now();
});
}
});
}
};
/**
* deleteServerDatabase: Removes the *.db file from the App-Group directory for iOS or the files directory on Android.
* Also, it sets the last_active_at to '0' entry in the 'servers' table from the APP database
* @param {string} serverUrl
* @returns {Promise<boolean>}
*/
deleteDatabase = async (serverUrl: string): Promise<boolean> => {
try {
const defaultDatabase = await this.getDefaultDatabase();
let server: IServers;
let result = true;
if (defaultDatabase) {
// NOTE: We are deleting this 'database' entry in the SERVERS entity on the DEFAULT database; for that we retrieve its record first.
const serversRecords = (await defaultDatabase.collections.get(SERVERS).query(Q.where('url', serverUrl)).fetch()) as IServers[];
server = serversRecords?.[0] ?? undefined;
const globalRecords = await defaultDatabase.collections.get(GLOBAL).query(Q.where('name', RECENTLY_VIEWED_SERVERS)).fetch() as IGlobal[];
const global = globalRecords?.[0] ?? undefined;
if (server) {
const databaseName = server.displayName;
// Perform a delete operation for this server record on the 'servers' table in default database
await defaultDatabase.action(async () => {
await server.destroyPermanently();
public deleteServerDatabase = async (serverUrl: string): Promise<void> => {
if (this.appDatabase?.database) {
const database = this.appDatabase?.database;
const server = await getServer(database, serverUrl);
if (server) {
database.action(() => {
server.update((record) => {
record.lastActiveAt = 0;
});
});
if (Platform.OS === 'ios') {
// On iOS, we'll delete the *.db file under the shared app-group/databases folder
deleteIOSDatabase({databaseName});
return true;
}
// On Android, we'll delete both the *.db file and the *.db-journal file
const androidFilesDir = `${this.androidFilesDirectory}databases/`;
const databaseFile = `${androidFilesDir}${databaseName}.db`;
const databaseJournal = `${androidFilesDir}${databaseName}.db-journal`;
await FileSystem.deleteAsync(databaseFile);
await FileSystem.deleteAsync(databaseJournal);
result = result && true;
}
if (global) {
// filter out the deleted serverURL
const urls = global.value as string[];
const filtered = urls.filter((url) => url !== serverUrl);
await defaultDatabase.action(async () => {
await global.update((record) => {
record.value = filtered;
});
});
result = result && true;
}
return result;
delete this.serverDatabases[serverUrl];
this.deleteServerDatabaseFiles(serverUrl);
}
return false;
} catch (e) {
return false;
}
};
}
/**
* destroyServerDatabase: Removes the *.db file from the App-Group directory for iOS or the files directory on Android.
* Also, removes the entry in the 'servers' table from the APP database
* @param {string} serverUrl
* @returns {Promise<boolean>}
*/
public destroyServerDatabase = async (serverUrl: string): Promise<void> => {
if (this.appDatabase?.database) {
const database = this.appDatabase?.database;
const server = await getServer(database, serverUrl);
if (server) {
database.action(async () => {
await server.destroyPermanently();
});
delete this.serverDatabases[serverUrl];
this.deleteServerDatabaseFiles(serverUrl);
}
}
}
/**
* deleteServerDatabaseFiles: Removes the *.db file from the App-Group directory for iOS or the files directory on Android.
* @param {string} serverUrl
* @returns {Promise<void>}
*/
private deleteServerDatabaseFiles = async (serverUrl: string): Promise<void> => {
const databaseName = hashCode(serverUrl);
if (Platform.OS === 'ios') {
// On iOS, we'll delete the *.db file under the shared app-group/databases folder
deleteIOSDatabase({databaseName});
return;
}
// On Android, we'll delete both the *.db file and the *.db-journal file
const androidFilesDir = `${this.databaseDirectory}databases/`;
const databaseFile = `${androidFilesDir}${databaseName}.db`;
const databaseJournal = `${androidFilesDir}${databaseName}.db-journal`;
await FileSystem.deleteAsync(databaseFile);
await FileSystem.deleteAsync(databaseJournal);
}
/**
* factoryReset: Removes the databases directory and all its contents on the respective platform
@@ -390,7 +340,7 @@ class DatabaseManager {
}
// On Android, we'll remove the databases folder under the Document Directory
const androidFilesDir = `${FileSystem.documentDirectory}databases/`;
const androidFilesDir = `${this.databaseDirectory}databases/`;
await FileSystem.deleteAsync(androidFilesDir);
return true;
} catch (e) {
@@ -398,74 +348,6 @@ class DatabaseManager {
}
};
/**
* isServerPresent : Confirms if the current serverUrl does not already exist in the database
* @param {String} serverUrl
* @returns {Promise<boolean>}
*/
private isServerPresent = async (serverUrl: string) => {
const allServers = await this.getAllServers([serverUrl]);
return allServers?.length > 0;
};
/**
* getAllServers : Retrieves all the servers registered in the default database
* @returns {Promise<Servers[]>}
*/
private getAllServers = async (serverUrls: string[]) => {
// Retrieve all server records from the default db
const defaultDatabase = await this.getDefaultDatabase();
if (defaultDatabase) {
const allServers = (await defaultDatabase.collections.get(SERVERS).query(Q.where('url', Q.oneOf(serverUrls))).fetch() as IServers[]);
return allServers;
}
return [];
};
/**
* setDefaultDatabase : Sets the default database.
* @returns {Promise<DatabaseInstance>}
*/
private setDefaultDatabase = async (): Promise<DatabaseInstance> => {
this.defaultDatabase = await this.createDatabaseConnection({
configs: {dbName: DEFAULT_DATABASE},
shouldAddToDefaultDatabase: false,
});
return this.defaultDatabase;
};
/**
* addServerToDefaultDatabase: Adds a record into the 'default' database - into the 'servers' table - for this new server connection
* @param {string} databaseFilePath
* @param {string} displayName
* @param {string} serverUrl
* @returns {Promise<void>}
*/
private addServerToDefaultDatabase = async ({databaseFilePath, displayName, serverUrl}: DefaultNewServerArgs) => {
try {
const defaultDatabase = await this.getDefaultDatabase();
const isServerPresent = await this.isServerPresent(serverUrl);
if (defaultDatabase && !isServerPresent) {
await defaultDatabase.action(async () => {
const serversCollection = defaultDatabase.collections.get(SERVERS);
await serversCollection.create((server: IServers) => {
server.dbPath = databaseFilePath;
server.displayName = displayName;
server.mentionCount = 0;
server.unreadCount = 0;
server.url = serverUrl;
});
});
}
} catch (e) {
// TODO : report to sentry? Show something on the UI ?
}
};
/**
* buildMigrationCallbacks: Creates a set of callbacks that can be used to monitor the migration process.
* For example, we can display a processing spinner while we have a migration going on. Moreover, we can also
@@ -507,7 +389,7 @@ class DatabaseManager {
* @returns {string}
*/
private getDatabaseFilePath = (dbName: string): string => {
return Platform.OS === 'ios' ? `${this.iOSAppGroupDatabase}/${dbName}.db` : `${FileSystem.documentDirectory}${dbName}.db`;
return Platform.OS === 'ios' ? `${this.databaseDirectory}/${dbName}.db` : `${this.databaseDirectory}${dbName}.db`;
};
}
@@ -517,4 +399,4 @@ if (!__DEV__) {
logger.silence();
}
export default DatabaseManager;
export default new DatabaseManager();

View File

@@ -4,233 +4,84 @@
import {Database, Q} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
import {DatabaseType} from '@typings/database/enums';
import IGlobal from '@typings/database/global';
import IServers from '@typings/database/servers';
import DatabaseManager from '@database/manager';
import ServerDataOperator from '@database/operator/server_data_operator';
jest.mock('@database/manager');
import type IServers from '@typings/database/models/app/servers';
const {GLOBAL, SERVERS} = MM_TABLES.DEFAULT;
const RECENTLY_VIEWED_SERVERS = 'RECENTLY_VIEWED_SERVERS';
const {SERVERS} = MM_TABLES.APP;
// NOTE : On the mock Database Manager, we cannot test for :
// 1. Android/iOS file path
// 2. Deletion of the 'databases' folder on those two platforms
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** Database Manager tests ***', () => {
let databaseManagerClient: DatabaseManager | null;
beforeEach(() => {
databaseManagerClient = new DatabaseManager();
const serverUrls = ['https://appv1.mattermost.com', 'https://appv2.mattermost.com'];
beforeAll(async () => {
await DatabaseManager.init(serverUrls);
});
afterEach(() => {
databaseManagerClient = null;
});
const createTwoConnections = async () => {
await databaseManagerClient!.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
dbName: 'connection1',
dbType: DatabaseType.SERVER,
serverUrl: 'https://appv1.mattermost.com',
},
});
await databaseManagerClient!.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
dbName: 'connection2',
dbType: DatabaseType.SERVER,
serverUrl: 'https://appv2.mattermost.com',
},
});
};
it('=> should return a default database', async () => {
expect.assertions(2);
const spyOnAddServerToDefaultDatabase = jest.spyOn(databaseManagerClient as any, 'addServerToDefaultDatabase');
const appDatabase = DatabaseManager.appDatabase?.database;
const defaultDB = await databaseManagerClient!.getDefaultDatabase();
expect(defaultDB).toBeInstanceOf(Database);
expect(spyOnAddServerToDefaultDatabase).not.toHaveBeenCalledTimes(1);
expect(appDatabase).toBeInstanceOf(Database);
expect(Object.keys(DatabaseManager.serverDatabases).length).toBe(2);
});
it('=> should create a new server connection', async () => {
expect.assertions(2);
it('=> should create a new server database', async () => {
expect.assertions(3);
const spyOnAddServerToDefaultDatabase = jest.spyOn(databaseManagerClient as any, 'addServerToDefaultDatabase');
const spyOnAddServerToDefaultDatabase = jest.spyOn(DatabaseManager as any, 'addServerToAppDatabase');
const connection1 = await databaseManagerClient!.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
const connection1 = await DatabaseManager!.createServerDatabase({
config: {
dbName: 'community mattermost',
dbType: DatabaseType.SERVER,
serverUrl: 'https://appv1.mattermost.com',
},
});
expect(connection1).toBeInstanceOf(Database);
expect(connection1?.database).toBeInstanceOf(Database);
expect(connection1?.operator).toBeInstanceOf(ServerDataOperator);
expect(spyOnAddServerToDefaultDatabase).toHaveBeenCalledTimes(1);
});
it('=> should switch between active server connections', async () => {
expect.assertions(6);
let adapter;
it('=> should switch between active servers', async () => {
expect.assertions(4);
const activeServerA = await databaseManagerClient!.getActiveServerDatabase();
let activeServerUrl = await DatabaseManager.getActiveServerUrl();
const serverA = await DatabaseManager.getActiveServerDatabase();
// as we haven't set an active server yet, we should be getting undefined in the activeServer variable
expect(activeServerA).toBeUndefined();
// as we haven't set an active server yet, so the first registered server should be the active one
expect(activeServerUrl).toBe(serverUrls[0]);
expect(serverA).toEqual(DatabaseManager.serverDatabases[serverUrls[0]].database);
const setActiveServer = async (serverUrl: string) => {
// now we set the active database
await databaseManagerClient!.setActiveServerDatabase(serverUrl);
};
await DatabaseManager.setActiveServerDatabase('https://appv2.mattermost.com');
await setActiveServer('https://appv1.mattermost.com');
// let's verify if we now have a value for activeServer
const activeServerB = await databaseManagerClient!.getActiveServerDatabase();
expect(activeServerB).toBeDefined();
adapter = activeServerB!.adapter as any;
const currentDBName = adapter.underlyingAdapter._dbName;
expect(currentDBName).toStrictEqual('appv1.mattermost.com');
// spice things up; we'll set a new server and verify if the value of activeServer changes
await setActiveServer('https://appv2.mattermost.com');
const activeServerC = await databaseManagerClient!.getActiveServerDatabase();
expect(activeServerC).toBeDefined();
adapter = activeServerC!.adapter as any;
const newDBName = adapter.underlyingAdapter._dbName;
expect(newDBName).toStrictEqual('appv2.mattermost.com');
const defaultDatabase = await databaseManagerClient!.getDefaultDatabase();
const records = await defaultDatabase!.collections.get(MM_TABLES.DEFAULT.GLOBAL).query(Q.where('name', 'RECENTLY_VIEWED_SERVERS')).fetch() as IGlobal[];
const recentlyViewedServers = records?.[0]?.value;
expect(recentlyViewedServers?.length).toBe(2);
// new active server should change and we have a Database and is active
activeServerUrl = await DatabaseManager.getActiveServerUrl();
const serverB = await DatabaseManager.getActiveServerDatabase();
expect(activeServerUrl).toBe(serverUrls[1]);
expect(serverB).toEqual(DatabaseManager.serverDatabases[serverUrls[1]].database);
});
it('=> should retrieve all database instances matching serverUrls parameter', async () => {
expect.assertions(3);
await createTwoConnections();
const spyOnCreateDatabaseConnection = jest.spyOn(databaseManagerClient!, 'createDatabaseConnection');
const dbInstances = await databaseManagerClient!.retrieveDatabaseInstances([
'https://xunity2.mattermost.com',
'https://appv2.mattermost.com',
'https://appv1.mattermost.com',
]);
expect(dbInstances).toBeTruthy();
const numDbInstances = dbInstances?.length ?? 0;
// The Database Manager will call the 'createDatabaseConnection' method in consequence of the number of database connection present in dbInstances array
expect(spyOnCreateDatabaseConnection).toHaveBeenCalledTimes(numDbInstances);
// We should have two active database connection
expect(numDbInstances).toEqual(2);
});
it('=> should retrieve existing database instances matching serverUrl parameter', async () => {
it('=> should delete appv1 server from the servers table of App database', async () => {
expect.assertions(2);
await createTwoConnections();
const spyOnRetrieveDatabaseInstances = jest.spyOn(databaseManagerClient!, 'retrieveDatabaseInstances');
const connection = await databaseManagerClient!.getDatabaseConnection({serverUrl: 'https://appv1.mattermost.com', setAsActiveDatabase: false});
expect(spyOnRetrieveDatabaseInstances).toHaveBeenCalledTimes(1);
expect(connection).toBeDefined();
});
//todo: test the current active database together with the getDatabaseConnection method
await DatabaseManager.setActiveServerDatabase('https://appv1.mattermost.com');
await DatabaseManager.destroyServerDatabase('https://appv1.mattermost.com');
it('=> should have records of Servers set in the servers table of the default database', async () => {
expect.assertions(3);
const defaultDB = await databaseManagerClient!.getDefaultDatabase();
expect(defaultDB).toBeDefined();
await createTwoConnections();
const serversRecords = await defaultDB!.collections.get(SERVERS).query().fetch() as IServers[];
expect(serversRecords).toBeDefined();
// We have call the 'DatabaseManager.setActiveServerDatabase' twice in the previous test case; that implies that we have 2 records in the 'servers' table
expect(serversRecords.length).toEqual(2);
});
it('=> should delete appv1 server from the servers table of Default database', async () => {
expect.assertions(3);
await createTwoConnections();
const defaultDatabase = await databaseManagerClient!.getDefaultDatabase();
await databaseManagerClient?.setActiveServerDatabase('https://appv1.mattermost.com');
await databaseManagerClient?.setActiveServerDatabase('https://appv2.mattermost.com');
const fetchGlobalRecords = async () => {
const initialGlobalRecords = await defaultDatabase!.collections.get(GLOBAL).query(Q.where('name', RECENTLY_VIEWED_SERVERS)).fetch() as IGlobal[];
return initialGlobalRecords?.[0].value as string[];
const fetchServerRecords = async (serverUrl: string) => {
const servers = await DatabaseManager.appDatabase?.database!.collections.get(SERVERS).query(Q.where('url', serverUrl)).fetch() as IServers[];
return servers.length;
};
const recentServers = await fetchGlobalRecords();
expect(recentServers.length).toBe(2);
const destroyed = await fetchServerRecords(serverUrls[0]);
expect(destroyed).toBe(0);
// Removing database for appv1 connection
const isAppV1Removed = await databaseManagerClient!.deleteDatabase('https://appv1.mattermost.com');
expect(isAppV1Removed).toBe(true);
// Verifying in the database to confirm if its record was deleted
const updatedRecentServers = await fetchGlobalRecords();
expect(updatedRecentServers.length).toBe(1);
});
it('=> should enforce uniqueness of connections using serverUrl as key', async () => {
expect.assertions(2);
// We can't have more than one connection with the same server url
const serverUrl = 'https://appv3.mattermost.com';
await databaseManagerClient!.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
dbName: 'community mattermost',
dbType: DatabaseType.SERVER,
serverUrl,
},
});
await databaseManagerClient!.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
dbName: 'duplicate server',
dbType: DatabaseType.SERVER,
serverUrl,
},
});
const defaultDB = await databaseManagerClient!.getDefaultDatabase();
const allServers = defaultDB && await defaultDB.collections.get(SERVERS).query().fetch() as IServers[];
// We should be having some servers returned here
expect(allServers?.length).toBeGreaterThan(0);
const occurrences = allServers?.map((server) => server.url).reduce((acc, cur) => (cur === serverUrl ? acc + 1 : acc), 0);
// We should only have one occurrence of the 'https://appv3.mattermost.com' url
expect(occurrences).toEqual(1);
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
expect(activeServerUrl).toEqual(serverUrls[1]);
});
});

View File

@@ -1,15 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Q} from '@nozbe/watermelondb';
import {Platform} from 'react-native';
import {MM_TABLES} from '@constants/database';
import {DatabaseType} from '@typings/database/enums';
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
import DatabaseManager from './index';
import type IServers from '@typings/database/models/app/servers';
export default async () => {
const databaseClient = new DatabaseManager();
await DatabaseManager.init([]);
// Test: It should return the iOS App-Group shared directory
const testAppGroupDirectory = () => {
@@ -18,17 +22,15 @@ export default async () => {
}
};
// Test: It should return an instance of the default database
const testGetDefaultDatabase = () => {
databaseClient.getDefaultDatabase();
// Test: It should return the app database
const testGetAppDatabase = () => {
return DatabaseManager.appDatabase?.database;
};
// Test: It should creates a new server connection
const testNewServerConnection = async () => {
await databaseClient.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
await DatabaseManager.createServerDatabase({
config: {
dbName: 'community mattermost',
dbType: DatabaseType.SERVER,
serverUrl: 'https://comm4.mattermost.com',
@@ -37,38 +39,44 @@ export default async () => {
};
// Test: It should return the current active server database
const testGetActiveServerConnection = () => {
// const activeServer = DatabaseManager.getActiveServerDatabase();
const testGetActiveServerConnection = async () => {
return DatabaseManager.getActiveServerDatabase();
};
// Test: It should set the current active server database to the provided server url.
const testSetActiveServerConnection = async () => {
await databaseClient.setActiveServerDatabase('https://comm4.mattermost.com');
await DatabaseManager.setActiveServerDatabase('https://comm4.mattermost.com');
};
// Test: It should return database instance(s) if there are valid server urls in the provided list.
const testRetrieveAllDatabaseConnections = async () => {
await databaseClient.retrieveDatabaseInstances([
'https://xunity2.mattermost.com',
'https://comm5.mattermost.com',
'https://comm4.mattermost.com',
]);
const database = DatabaseManager.appDatabase?.database;
const servers = (await database?.collections.get(MM_TABLES.APP.SERVERS).
query(Q.where(
'url',
Q.oneOf([
'https://xunity2.mattermost.com',
'https://comm5.mattermost.com',
'https://comm4.mattermost.com',
]),
)).fetch()) as IServers[];
return servers;
};
// Test: It should delete the associated *.db file for this server url
const testDeleteSQLFile = async () => {
await databaseClient.deleteDatabase('https://comm4.mattermost.com');
await DatabaseManager.deleteServerDatabase('https://comm4.mattermost.com');
};
// Test: It should wipe out the databases folder under the documents direction on Android and in the shared directory for the AppGroup on iOS
const testFactoryReset = async () => {
await databaseClient.factoryReset(true);
await DatabaseManager.factoryReset(true);
};
// NOTE : Comment and test the below functions one at a time. It starts with creating a default database and ends with a factory reset.
testAppGroupDirectory();
testGetDefaultDatabase();
testGetAppDatabase();
await testNewServerConnection();
testGetActiveServerConnection();
await testSetActiveServerConnection();

View File

@@ -6,7 +6,7 @@ import {field, json} from '@nozbe/watermelondb/decorators';
import {MM_TABLES} from '@constants/database';
const {GLOBAL} = MM_TABLES.DEFAULT;
const {GLOBAL} = MM_TABLES.APP;
// TODO : add TS definitions to sanitizer function signature.
@@ -15,7 +15,7 @@ const {GLOBAL} = MM_TABLES.DEFAULT;
* data type. It will hold information that applies to the whole app ( e.g. sidebar settings for tablets)
*/
export default class Global extends Model {
/** table (entity name) : global */
/** table (name) : global */
static table = GLOBAL;
/** name : The label/key to use to retrieve the special 'value' */

View File

@@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export {default as App} from './app';
export {default as Info} from './info';
export {default as Global} from './global';
export {default as Servers} from './servers';

View File

@@ -6,15 +6,15 @@ import {field} from '@nozbe/watermelondb/decorators';
import {MM_TABLES} from '@constants/database';
const {APP} = MM_TABLES.DEFAULT;
const {INFO} = MM_TABLES.APP;
/**
* The App model will hold information - such as the version number, build number and creation date -
* for the Mattermost mobile app.
*/
export default class App extends Model {
/** table (entity name) : app */
static table = APP;
export default class Info extends Model {
/** table (name) : info */
static table = INFO;
/** build_number : Build number for the app */
@field('build_number') buildNumber!: string;

View File

@@ -6,14 +6,14 @@ import {field} from '@nozbe/watermelondb/decorators';
import {MM_TABLES} from '@constants/database';
const {SERVERS} = MM_TABLES.DEFAULT;
const {SERVERS} = MM_TABLES.APP;
/**
* The Server model will help us to identify the various servers a user will log in; in the context of
* multi-server support system. The db_path field will hold the App-Groups file-path
*/
export default class Servers extends Model {
/** table (entity name) : servers */
/** table (name) : servers */
static table = SERVERS;
/** db_path : The file path where the database is stored */

View File

@@ -6,16 +6,16 @@ import {children, field, immutableRelation, lazy} from '@nozbe/watermelondb/deco
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import ChannelInfo from '@typings/database/channel_info';
import ChannelMembership from '@typings/database/channel_membership';
import Draft from '@typings/database/draft';
import GroupsInChannel from '@typings/database/groups_in_channel';
import MyChannel from '@typings/database/my_channel';
import MyChannelSettings from '@typings/database/my_channel_settings';
import Post from '@typings/database/post';
import PostsInChannel from '@typings/database/posts_in_channel';
import Team from '@typings/database/team';
import User from '@typings/database/user';
import ChannelInfo from '@typings/database/models/servers/channel_info';
import ChannelMembership from '@typings/database/models/servers/channel_membership';
import Draft from '@typings/database/models/servers/draft';
import GroupsInChannel from '@typings/database/models/servers/groups_in_channel';
import MyChannel from '@typings/database/models/servers/my_channel';
import MyChannelSettings from '@typings/database/models/servers/my_channel_settings';
import Post from '@typings/database/models/servers/post';
import PostsInChannel from '@typings/database/models/servers/posts_in_channel';
import Team from '@typings/database/models/servers/team';
import User from '@typings/database/models/servers/user';
const {
CHANNEL,
@@ -35,10 +35,10 @@ const {
* The Channel model represents a channel in the Mattermost app.
*/
export default class Channel extends Model {
/** table (entity name) : Channel */
/** table (name) : Channel */
static table = CHANNEL;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A CHANNEL is associated with only one CHANNEL_INFO (relationship is 1:1) */
@@ -123,7 +123,7 @@ export default class Channel extends Model {
/** creator : The USER who created this CHANNEL*/
@immutableRelation(USER, 'creator_id') creator!: Relation<User>;
/** info : Query returning extra information about this channel from entity CHANNEL_INFO */
/** info : Query returning extra information about this channel from CHANNEL_INFO table */
@lazy info = this.collections.get(CHANNEL_INFO).query(Q.on(CHANNEL, 'id', this.id)) as Query<ChannelInfo>;
/** membership : Query returning the membership data for the current user if it belongs to this channel */

View File

@@ -6,20 +6,20 @@ import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Channel from '@typings/database/channel';
import Channel from '@typings/database/models/servers/channel';
const {CHANNEL, CHANNEL_INFO} = MM_TABLES.SERVER;
/**
* ChannelInfo is an extension of the information contained in the Channel entity.
* ChannelInfo is an extension of the information contained in the Channel table.
* In a Separation of Concerns approach, ChannelInfo will provide additional information about a channel but on a more
* specific level.
*/
export default class ChannelInfo extends Model {
/** table (entity name) : ChannelInfo */
/** table (name) : ChannelInfo */
static table = CHANNEL_INFO;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A CHANNEL is associated with only one CHANNEL_INFO (relationship is 1:1) */
@@ -44,6 +44,6 @@ export default class ChannelInfo extends Model {
/** purpose: The intention behind this channel */
@field('purpose') purpose!: string;
/** channel : The lazy query property to the record from entity CHANNEL */
/** channel : The lazy query property to the record from CHANNEL table */
@immutableRelation(CHANNEL, 'channel_id') channel!: Relation<Channel>;
}

View File

@@ -5,9 +5,9 @@ import {Q, Query, Relation} from '@nozbe/watermelondb';
import {field, immutableRelation, lazy} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import Channel from '@typings/database/channel';
import Channel from '@typings/database/models/servers/channel';
import {MM_TABLES} from '@constants/database';
import User from '@typings/database/user';
import User from '@typings/database/models/servers/user';
const {CHANNEL, CHANNEL_MEMBERSHIP, USER} = MM_TABLES.SERVER;
@@ -16,10 +16,10 @@ const {CHANNEL, CHANNEL_MEMBERSHIP, USER} = MM_TABLES.SERVER;
* channels ( N:N relationship between model Users and model Channel)
*/
export default class ChannelMembership extends Model {
/** table (entity name) : ChannelMembership */
/** table (name) : ChannelMembership */
static table = CHANNEL_MEMBERSHIP;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A CHANNEL can have multiple USER */

View File

@@ -10,7 +10,7 @@ const {CUSTOM_EMOJI} = MM_TABLES.SERVER;
/** The CustomEmoji model describes all the custom emojis used in the Mattermost app */
export default class CustomEmoji extends Model {
/** table (entity name) : CustomEmoji */
/** table (name) : CustomEmoji */
static table = CUSTOM_EMOJI;
/** name : The custom emoji's name*/

View File

@@ -12,10 +12,10 @@ const {CHANNEL, DRAFT, POST} = MM_TABLES.SERVER;
* The Draft model represents the draft state of messages in Direct/Group messages and in channels
*/
export default class Draft extends Model {
/** table (entity name) : Draft */
/** table (name) : Draft */
static table = DRAFT;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A DRAFT can belong to only one CHANNEL */
@@ -34,6 +34,6 @@ export default class Draft extends Model {
/** root_id : The root_id will be empty most of the time unless the draft relates to a draft reply of a thread */
@field('root_id') rootId!: string;
/** files : The files field will hold an array of file objects that have not yet been uploaded and persisted within the FILE entity */
/** files : The files field will hold an array of file objects that have not yet been uploaded and persisted within the FILE table */
@json('files', (rawJson) => rawJson) files!: FileInfo[];
}

View File

@@ -6,7 +6,7 @@ import Model, {Associations} from '@nozbe/watermelondb/Model';
import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
import {MM_TABLES} from '@constants/database';
import Post from '@typings/database/post';
import Post from '@typings/database/models/servers/post';
const {FILE, POST} = MM_TABLES.SERVER;
@@ -14,10 +14,10 @@ const {FILE, POST} = MM_TABLES.SERVER;
* The File model works in pair with the Post model. It hosts information about the files shared in a Post
*/
export default class File extends Model {
/** table (entity name) : File */
/** table (name) : File */
static table = FILE;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A POST has a 1:N relationship with FILE. */

View File

@@ -5,9 +5,9 @@ import Model, {Associations} from '@nozbe/watermelondb/Model';
import {children, field} from '@nozbe/watermelondb/decorators';
import {MM_TABLES} from '@constants/database';
import GroupMembership from '@typings/database/group_membership';
import GroupsInChannel from '@typings/database/groups_in_channel';
import GroupsInTeam from '@typings/database/groups_in_team';
import GroupMembership from '@typings/database/models/servers/group_membership';
import GroupsInChannel from '@typings/database/models/servers/groups_in_channel';
import GroupsInTeam from '@typings/database/models/servers/groups_in_team';
const {GROUP, GROUPS_IN_CHANNEL, GROUPS_IN_TEAM, GROUP_MEMBERSHIP} = MM_TABLES.SERVER;
@@ -17,10 +17,10 @@ const {GROUP, GROUPS_IN_CHANNEL, GROUPS_IN_TEAM, GROUP_MEMBERSHIP} = MM_TABLES.S
* name in the message. (e.g @mobile_team)
*/
export default class Group extends Model {
/** table (entity name) : Group */
/** table (name) : Group */
static table = GROUP;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A GROUP has a 1:N relationship with GROUPS_IN_CHANNEL */

View File

@@ -6,8 +6,8 @@ import {field, immutableRelation, lazy} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Group from '@typings/database/group';
import User from '@typings/database/user';
import Group from '@typings/database/models/servers/group';
import User from '@typings/database/models/servers/user';
const {GROUP, GROUP_MEMBERSHIP, USER} = MM_TABLES.SERVER;
@@ -16,10 +16,10 @@ const {GROUP, GROUP_MEMBERSHIP, USER} = MM_TABLES.SERVER;
* groups (relationship type N:N)
*/
export default class GroupMembership extends Model {
/** table (entity name) : GroupMembership */
/** table (name) : GroupMembership */
static table = GROUP_MEMBERSHIP;
/** associations : Describes every relationship to this entity */
/** associations : Describes every relationship to this table */
static associations: Associations = {
/** A GROUP can have multiple users in it */

View File

@@ -6,8 +6,8 @@ import Model, {Associations} from '@nozbe/watermelondb/Model';
import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
import {MM_TABLES} from '@constants/database';
import Channel from '@typings/database/channel';
import Group from '@typings/database/group';
import Channel from '@typings/database/models/servers/channel';
import Group from '@typings/database/models/servers/group';
const {GROUP, GROUPS_IN_CHANNEL, CHANNEL} = MM_TABLES.SERVER;
@@ -15,10 +15,10 @@ const {GROUP, GROUPS_IN_CHANNEL, CHANNEL} = MM_TABLES.SERVER;
* The GroupsInChannel links the Channel model with the Group model
*/
export default class GroupsInChannel extends Model {
/** table (entity name) : GroupsInChannel */
/** table (name) : GroupsInChannel */
static table = GROUPS_IN_CHANNEL;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A GROUP can be associated with multiple GROUPS_IN_CHANNEL (relationship is 1:N) */

View File

@@ -5,9 +5,9 @@ import {Relation} from '@nozbe/watermelondb';
import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import Group from '@typings/database/group';
import Group from '@typings/database/models/servers/group';
import {MM_TABLES} from '@constants/database';
import Team from '@typings/database/team';
import Team from '@typings/database/models/servers/team';
const {GROUP, GROUPS_IN_TEAM, TEAM} = MM_TABLES.SERVER;
@@ -15,10 +15,10 @@ const {GROUP, GROUPS_IN_TEAM, TEAM} = MM_TABLES.SERVER;
* The GroupsInTeam links the Team model with the Group model
*/
export default class GroupsInTeam extends Model {
/** table (entity name) : GroupsInTeam */
/** table (name) : GroupsInTeam */
static table = GROUPS_IN_TEAM;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** GroupsInTeam can belong to only one Group */

View File

@@ -5,7 +5,7 @@ import {Relation} from '@nozbe/watermelondb';
import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import Channel from '@typings/database/channel';
import Channel from '@typings/database/models/servers/channel';
import {MM_TABLES} from '@constants/database';
const {CHANNEL, MY_CHANNEL} = MM_TABLES.SERVER;
@@ -14,13 +14,13 @@ const {CHANNEL, MY_CHANNEL} = MM_TABLES.SERVER;
* MyChannel is an extension of the Channel model but it lists only the Channels the app's user belongs to
*/
export default class MyChannel extends Model {
/** table (entity name) : MyChannel */
/** table (name) : MyChannel */
static table = MY_CHANNEL;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A CHANNEL can be associated to only one record from entity MY_CHANNEL (relationship is 1:1) */
/** A CHANNEL can be associated to only one record from the MY_CHANNEL table (relationship is 1:1) */
[CHANNEL]: {type: 'belongs_to', key: 'channel_id'},
};
@@ -42,6 +42,6 @@ export default class MyChannel extends Model {
/** roles : The user's privileges on this channel */
@field('roles') roles!: string;
/** channel : The relation pointing to entity CHANNEL */
/** channel : The relation pointing to the CHANNEL table */
@immutableRelation(CHANNEL, 'channel_id') channel!: Relation<Channel>;
}

View File

@@ -5,7 +5,7 @@ import {Relation} from '@nozbe/watermelondb';
import {field, immutableRelation, json} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import Channel from '@typings/database/channel';
import Channel from '@typings/database/models/servers/channel';
import {MM_TABLES} from '@constants/database';
const {CHANNEL, MY_CHANNEL_SETTINGS} = MM_TABLES.SERVER;
@@ -15,10 +15,10 @@ const {CHANNEL, MY_CHANNEL_SETTINGS} = MM_TABLES.SERVER;
* the channel this user belongs to.
*/
export default class MyChannelSettings extends Model {
/** table (entity name) : MyChannelSettings */
/** table (name) : MyChannelSettings */
static table = MY_CHANNEL_SETTINGS;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A CHANNEL is related to only one MY_CHANNEL_SETTINGS (relationship is 1:1) */
@@ -31,6 +31,6 @@ export default class MyChannelSettings extends Model {
/** notify_props : Configurations with regards to this channel */
@json('notify_props', (rawJson) => rawJson) notifyProps!: NotifyProps;
/** channel : The relation pointing to entity CHANNEL */
/** channel : The relation pointing to the CHANNEL table */
@immutableRelation(CHANNEL, 'channel_id') channel!: Relation<Channel>;
}

View File

@@ -6,7 +6,7 @@ import {field, relation} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Team from '@typings/database/team';
import Team from '@typings/database/models/servers/team';
const {TEAM, MY_TEAM} = MM_TABLES.SERVER;
@@ -14,10 +14,10 @@ const {TEAM, MY_TEAM} = MM_TABLES.SERVER;
* MyTeam represents only the teams that the current user belongs to
*/
export default class MyTeam extends Model {
/** table (entity name) : MyTeam */
/** table (name) : MyTeam */
static table = MY_TEAM;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** TEAM and MY_TEAM have a 1:1 relationship. */
@@ -33,9 +33,9 @@ export default class MyTeam extends Model {
/** roles : The different permissions that this user has in the team, concatenated together with comma to form a single string. */
@field('roles') roles!: string;
/** team_id : The foreign key of the 'parent' Team entity */
/** team_id : The foreign key of the 'parent' Team record */
@field('team_id') teamId!: string;
/** team : The relation to the entity TEAM, that this user belongs to */
/** team : The relation to the TEAM, that this user belongs to */
@relation(MY_TEAM, 'team_id') team!: Relation<Team>;
}

View File

@@ -6,13 +6,13 @@ import {children, field, immutableRelation, json, lazy} from '@nozbe/watermelond
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Channel from '@typings/database/channel';
import Draft from '@typings/database/draft';
import File from '@typings/database/file';
import PostInThread from '@typings/database/posts_in_thread';
import PostMetadata from '@typings/database/post_metadata';
import Reaction from '@typings/database/reaction';
import User from '@typings/database/user';
import Channel from '@typings/database/models/servers/channel';
import Draft from '@typings/database/models/servers/draft';
import File from '@typings/database/models/servers/file';
import PostInThread from '@typings/database/models/servers/posts_in_thread';
import PostMetadata from '@typings/database/models/servers/post_metadata';
import Reaction from '@typings/database/models/servers/reaction';
import User from '@typings/database/models/servers/user';
const {CHANNEL, DRAFT, FILE, POST, POSTS_IN_THREAD, POST_METADATA, REACTION, USER} = MM_TABLES.SERVER;
@@ -20,10 +20,10 @@ const {CHANNEL, DRAFT, FILE, POST, POSTS_IN_THREAD, POST_METADATA, REACTION, USE
* The Post model is the building block of communication in the Mattermost app.
*/
export default class Post extends Model {
/** table (entity name) : Post */
/** table (name) : Post */
static table = POST;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A CHANNEL can have multiple POST. (relationship is 1:N) */

View File

@@ -7,7 +7,7 @@ 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';
import Post from '@typings/database/models/servers/post';
const {POST, POST_METADATA} = MM_TABLES.SERVER;
@@ -15,10 +15,10 @@ const {POST, POST_METADATA} = MM_TABLES.SERVER;
* PostMetadata provides additional information on a POST
*/
export default class PostMetadata extends Model {
/** table (entity name) : PostMetadata */
/** table (name) : PostMetadata */
static table = POST_METADATA;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A POST can have multiple POST_METADATA.(relationship is 1:N)*/

View File

@@ -6,7 +6,7 @@ import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Channel from '@typings/database/channel';
import Channel from '@typings/database/models/servers/channel';
const {CHANNEL, POSTS_IN_CHANNEL} = MM_TABLES.SERVER;
@@ -15,10 +15,10 @@ const {CHANNEL, POSTS_IN_CHANNEL} = MM_TABLES.SERVER;
* gaps in between for an efficient user reading experience of posts.
*/
export default class PostsInChannel extends Model {
/** table (entity name) : PostsInChannel */
/** table (name) : PostsInChannel */
static table = POSTS_IN_CHANNEL;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A CHANNEL can have multiple POSTS_IN_CHANNEL. (relationship is 1:N)*/

View File

@@ -6,7 +6,7 @@ import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Post from '@typings/database/post';
import Post from '@typings/database/models/servers/post';
const {POST, POSTS_IN_THREAD} = MM_TABLES.SERVER;
@@ -15,10 +15,10 @@ const {POST, POSTS_IN_THREAD} = MM_TABLES.SERVER;
* gaps in between for an efficient user reading experience for threads.
*/
export default class PostsInThread extends Model {
/** table (entity name) : PostsInThread */
/** table (name) : PostsInThread */
static table = POSTS_IN_THREAD;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A POST can have multiple POSTS_IN_THREAD.(relationship is 1:N)*/

View File

@@ -6,7 +6,7 @@ import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import User from '@typings/database/user';
import User from '@typings/database/models/servers/user';
const {PREFERENCE, USER} = MM_TABLES.SERVER;
@@ -15,10 +15,10 @@ const {PREFERENCE, USER} = MM_TABLES.SERVER;
* This includes settings about the account, the themes, etc.
*/
export default class Preference extends Model {
/** table (entity name) : Preference */
/** table (name) : Preference */
static table = PREFERENCE;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A USER can have multiple PREFERENCE.(relationship is 1:N)*/

View File

@@ -6,8 +6,8 @@ import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Post from '@typings/database/post';
import User from '@typings/database/user';
import Post from '@typings/database/models/servers/post';
import User from '@typings/database/models/servers/user';
const {POST, REACTION, USER} = MM_TABLES.SERVER;
@@ -15,10 +15,10 @@ const {POST, REACTION, USER} = MM_TABLES.SERVER;
* The Reaction Model is used to present the reactions a user had on a particular post
*/
export default class Reaction extends Model {
/** table (entity name) : Reaction */
/** table (name) : Reaction */
static table = REACTION;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A POST can have multiple REACTION. (relationship is 1:N) */

View File

@@ -10,7 +10,7 @@ const {ROLE} = MM_TABLES.SERVER;
/** The Role model will describe the set of permissions for each role */
export default class Role extends Model {
/** table (entity name) : Role */
/** table (name) : Role */
static table = ROLE;
/** name : The role's name */

View File

@@ -6,7 +6,7 @@ import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Team from '@typings/database/team';
import Team from '@typings/database/models/servers/team';
const {SLASH_COMMAND, TEAM} = MM_TABLES.SERVER;
@@ -14,10 +14,10 @@ const {SLASH_COMMAND, TEAM} = MM_TABLES.SERVER;
* The SlashCommand model describes the commands of the various commands available in each team.
*/
export default class SlashCommand extends Model {
/** table (entity name) : SlashCommand */
/** table (name) : SlashCommand */
static table = SLASH_COMMAND;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A TEAM can have multiple slash commands. (relationship is 1:N) */

View File

@@ -14,7 +14,7 @@ const {SYSTEM} = MM_TABLES.SERVER;
* custom data (e.g. recent emoji used)
*/
export default class System extends Model {
/** table (entity name) : System */
/** table (name) : System */
static table = SYSTEM;
/** name : The name or key value for the config */

View File

@@ -6,13 +6,13 @@ import {children, field, lazy} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Channel from '@typings/database/channel';
import GroupsInTeam from '@typings/database/groups_in_team';
import MyTeam from '@typings/database/my_team';
import SlashCommand from '@typings/database/slash_command';
import TeamChannelHistory from '@typings/database/team_channel_history';
import TeamMembership from '@typings/database/team_membership';
import TeamSearchHistory from '@typings/database/team_search_history';
import Channel from '@typings/database/models/servers/channel';
import GroupsInTeam from '@typings/database/models/servers/groups_in_team';
import MyTeam from '@typings/database/models/servers/my_team';
import SlashCommand from '@typings/database/models/servers/slash_command';
import TeamChannelHistory from '@typings/database/models/servers/team_channel_history';
import TeamMembership from '@typings/database/models/servers/team_membership';
import TeamSearchHistory from '@typings/database/models/servers/team_search_history';
const {
CHANNEL,
@@ -29,10 +29,10 @@ const {
* A Team houses and enables communication to happen across channels and users.
*/
export default class Team extends Model {
/** table (entity name) : Team */
/** table (name) : Team */
static table = TEAM;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A TEAM has a 1:N relationship with CHANNEL. A TEAM can possess multiple channels */

View File

@@ -6,7 +6,7 @@ import {field, immutableRelation, json} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Team from '@typings/database/team';
import Team from '@typings/database/models/servers/team';
const {TEAM, TEAM_CHANNEL_HISTORY} = MM_TABLES.SERVER;
@@ -15,10 +15,10 @@ const {TEAM, TEAM_CHANNEL_HISTORY} = MM_TABLES.SERVER;
* by the user.
*/
export default class TeamChannelHistory extends Model {
/** table (entity name) : TeamChannelHistory */
/** table (name) : TeamChannelHistory */
static table = TEAM_CHANNEL_HISTORY;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A TEAM and TEAM_CHANNEL_HISTORY share a 1:1 relationship */

View File

@@ -6,8 +6,8 @@ import {field, immutableRelation, lazy} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Team from '@typings/database/team';
import User from '@typings/database/user';
import Team from '@typings/database/models/servers/team';
import User from '@typings/database/models/servers/user';
const {TEAM, TEAM_MEMBERSHIP, USER} = MM_TABLES.SERVER;
@@ -16,10 +16,10 @@ const {TEAM, TEAM_MEMBERSHIP, USER} = MM_TABLES.SERVER;
* teams (relationship type N:N)
*/
export default class TeamMembership extends Model {
/** table (entity name) : TeamMembership */
/** table (name) : TeamMembership */
static table = TEAM_MEMBERSHIP;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** TEAM and TEAM_MEMBERSHIP share a 1:N relationship; USER can be part of multiple teams */

View File

@@ -6,7 +6,7 @@ import {field, immutableRelation, text} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Team from '@typings/database/team';
import Team from '@typings/database/models/servers/team';
const {TEAM, TEAM_SEARCH_HISTORY} = MM_TABLES.SERVER;
@@ -15,10 +15,10 @@ const {TEAM, TEAM_SEARCH_HISTORY} = MM_TABLES.SERVER;
* at team level in the app.
*/
export default class TeamSearchHistory extends Model {
/** table (entity name) : TeamSearchHistory */
/** table (name) : TeamSearchHistory */
static table = TEAM_SEARCH_HISTORY;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A TEAM can have multiple search terms */

View File

@@ -12,7 +12,7 @@ const {TERMS_OF_SERVICE} = MM_TABLES.SERVER;
* The model for Terms of Service
*/
export default class TermsOfService extends Model {
/** table (entity name) : TermsOfService */
/** table (name) : TermsOfService */
static table = TERMS_OF_SERVICE;
/** accepted_at : the date the term has been accepted */

View File

@@ -5,13 +5,13 @@ import {children, field, json} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Channel from '@typings/database/channel';
import ChannelMembership from '@typings/database/channel_membership';
import GroupMembership from '@typings/database/group_membership';
import Post from '@typings/database/post';
import Preference from '@typings/database/preference';
import Reaction from '@typings/database/reaction';
import TeamMembership from '@typings/database/team_membership';
import Channel from '@typings/database/models/servers/channel';
import ChannelMembership from '@typings/database/models/servers/channel_membership';
import GroupMembership from '@typings/database/models/servers/group_membership';
import Post from '@typings/database/models/servers/post';
import Preference from '@typings/database/models/servers/preference';
import Reaction from '@typings/database/models/servers/reaction';
import TeamMembership from '@typings/database/models/servers/team_membership';
const {
CHANNEL,
@@ -25,14 +25,14 @@ const {
} = MM_TABLES.SERVER;
/**
* The User model represents the 'USER' entity and its relationship to other
* The User model represents the 'USER' table and its relationship to other
* shareholders in the app.
*/
export default class User extends Model {
/** table (entity name) : User */
/** table (name) : User */
static table = USER;
/** associations : Describes every relationship to this entity. */
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** USER has a 1:N relationship with CHANNEL. A user can create multiple channels */

View File

@@ -0,0 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Info from '@typings/database/models/app/info';
import {RawInfo, RawGlobal, RawServers} from '@typings/database/database';
import Global from '@typings/database/models/app/global';
import Servers from '@typings/database/models/app/servers';
export const isRecordInfoEqualToRaw = (record: Info, raw: RawInfo) => {
return (raw.build_number === record.buildNumber && raw.version_number === record.versionNumber);
};
export const isRecordGlobalEqualToRaw = (record: Global, raw: RawGlobal) => {
return raw.name === record.name && raw.value === record.value;
};
export const isRecordServerEqualToRaw = (record: Servers, raw: RawServers) => {
return raw.url === record.url && raw.db_path === record.dbPath;
};

View File

@@ -0,0 +1,136 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {
isRecordInfoEqualToRaw,
isRecordGlobalEqualToRaw,
isRecordServerEqualToRaw,
} from '@database/operator/app_data_operator/comparator';
import {
transformInfoRecord,
transformGlobalRecord,
transformServersRecord,
} from '@database/operator/app_data_operator/transformers';
import {RawGlobal, RawServers} from '@typings/database/database';
describe('** APP DATA OPERATOR **', () => {
beforeAll(async () => {
await DatabaseManager.init([]);
});
it('=> HandleApp: should write to INFO table', async () => {
const appDatabase = DatabaseManager.appDatabase?.database;
const appOperator = DatabaseManager.appDatabase?.operator;
expect(appDatabase).toBeTruthy();
expect(appOperator).toBeTruthy();
const spyOnHandleRecords = jest.spyOn(appOperator as any, 'handleRecords');
await appOperator?.handleInfo({
info: [
{
build_number: 'build-10x',
created_at: 1,
version_number: 'version-10',
},
{
build_number: 'build-11y',
created_at: 1,
version_number: 'version-11',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'version_number',
transformer: transformInfoRecord,
findMatchingRecordBy: isRecordInfoEqualToRaw,
createOrUpdateRawValues: [
{
build_number: 'build-10x',
created_at: 1,
version_number: 'version-10',
},
{
build_number: 'build-11y',
created_at: 1,
version_number: 'version-11',
},
],
tableName: 'Info',
prepareRecordsOnly: false,
});
});
it('=> HandleGlobal: should write to GLOBAL table', async () => {
const appDatabase = DatabaseManager.appDatabase?.database;
const appOperator = DatabaseManager.appDatabase?.operator;
expect(appDatabase).toBeTruthy();
expect(appOperator).toBeTruthy();
const spyOnHandleRecords = jest.spyOn(appOperator as any, 'handleRecords');
const global: RawGlobal[] = [{name: 'global-1-name', value: 'global-1-value'}];
await appOperator?.handleGlobal({
global,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledWith({
findMatchingRecordBy: isRecordGlobalEqualToRaw,
fieldName: 'name',
transformer: transformGlobalRecord,
createOrUpdateRawValues: global,
tableName: 'Global',
prepareRecordsOnly: false,
});
});
it('=> HandleServers: should write to SERVERS table', async () => {
const appDatabase = DatabaseManager.appDatabase?.database;
const appOperator = DatabaseManager.appDatabase?.operator;
expect(appDatabase).toBeTruthy();
expect(appOperator).toBeTruthy();
const spyOnHandleRecords = jest.spyOn(appOperator as any, 'handleRecords');
const servers: RawServers[] = [
{
db_path: 'server.db',
display_name: 'community',
mention_count: 0,
unread_count: 0,
url: 'https://community.mattermost.com',
isSecured: true,
lastActiveAt: 1623926359,
},
];
await appOperator?.handleServers({
servers,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'url',
transformer: transformServersRecord,
findMatchingRecordBy: isRecordServerEqualToRaw,
createOrUpdateRawValues: [
{
db_path: 'server.db',
display_name: 'community',
mention_count: 0,
unread_count: 0,
url: 'https://community.mattermost.com',
isSecured: true,
lastActiveAt: 1623926359,
},
],
tableName: 'Servers',
prepareRecordsOnly: false,
});
});
});

View File

@@ -0,0 +1,83 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import DataOperatorException from '@database/exceptions/data_operator_exception';
import {
isRecordInfoEqualToRaw,
isRecordGlobalEqualToRaw,
isRecordServerEqualToRaw,
} from '@database/operator/app_data_operator/comparator';
import {
transformInfoRecord,
transformGlobalRecord,
transformServersRecord,
} from '@database/operator/app_data_operator/transformers';
import BaseDataOperator from '@database/operator/base_data_operator';
import {getUniqueRawsBy} from '@database/operator/utils/general';
import {
HandleInfoArgs,
HandleGlobalArgs,
HandleServersArgs,
} from '@typings/database/database';
const {APP: {INFO, GLOBAL, SERVERS}} = MM_TABLES;
export default class AppDataOperator extends BaseDataOperator {
handleInfo = async ({info, prepareRecordsOnly = true}: HandleInfoArgs) => {
if (!info.length) {
throw new DataOperatorException(
'An empty "values" array has been passed to the handleInfo',
);
}
const records = await this.handleRecords({
fieldName: 'version_number',
findMatchingRecordBy: isRecordInfoEqualToRaw,
transformer: transformInfoRecord,
prepareRecordsOnly,
createOrUpdateRawValues: getUniqueRawsBy({raws: info, key: 'version_number'}),
tableName: INFO,
});
return records;
}
handleGlobal = async ({global, prepareRecordsOnly = true}: HandleGlobalArgs) => {
if (!global.length) {
throw new DataOperatorException(
'An empty "values" array has been passed to the handleGlobal',
);
}
const records = await this.handleRecords({
fieldName: 'name',
findMatchingRecordBy: isRecordGlobalEqualToRaw,
transformer: transformGlobalRecord,
prepareRecordsOnly,
createOrUpdateRawValues: getUniqueRawsBy({raws: global, key: 'name'}),
tableName: GLOBAL,
});
return records;
}
handleServers = async ({servers, prepareRecordsOnly = true}: HandleServersArgs) => {
if (!servers.length) {
throw new DataOperatorException(
'An empty "values" array has been passed to the handleServers',
);
}
const records = await this.handleRecords({
fieldName: 'url',
findMatchingRecordBy: isRecordServerEqualToRaw,
transformer: transformServersRecord,
prepareRecordsOnly,
createOrUpdateRawValues: getUniqueRawsBy({raws: servers, key: 'display_name'}),
tableName: SERVERS,
});
return records;
}
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers';
import Info from '@typings/database/models/app/info';
import {TransformerArgs, RawInfo, RawGlobal, RawServers} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
import Global from '@typings/database/models/app/global';
import Servers from '@typings/database/models/app/servers';
const {INFO, GLOBAL, SERVERS} = MM_TABLES.APP;
/**
* transformInfoRecord: Prepares a record of the APP database 'Info' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformInfoRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawInfo;
const record = value.record as Info;
const isCreateAction = action === OperationType.CREATE;
const fieldsMapper = (app: Info) => {
app._raw.id = isCreateAction ? app.id : record.id;
app.buildNumber = raw?.build_number;
app.createdAt = raw?.created_at;
app.versionNumber = raw?.version_number;
};
return prepareBaseRecord({
action,
database,
fieldsMapper,
tableName: INFO,
value,
});
};
/**
* transformGlobalRecord: Prepares a record of the APP database 'Global' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformGlobalRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawGlobal;
const record = value.record as Global;
const isCreateAction = action === OperationType.CREATE;
const fieldsMapper = (global: Global) => {
global._raw.id = isCreateAction ? global.id : record.id;
global.name = raw?.name;
global.value = raw?.value;
};
return prepareBaseRecord({
action,
database,
fieldsMapper,
tableName: GLOBAL,
value,
});
};
/**
* transformServersRecord: Prepares a record of the APP database 'Servers' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformServersRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawServers;
const record = value.record as Servers;
const isCreateAction = action === OperationType.CREATE;
const fieldsMapper = (servers: Servers) => {
servers._raw.id = isCreateAction ? servers.id : record.id;
servers.dbPath = raw?.db_path;
servers.displayName = raw?.display_name;
servers.mentionCount = raw?.mention_count;
servers.unreadCount = raw?.unread_count;
servers.url = raw?.url;
servers.isSecured = raw?.isSecured;
servers.lastActiveAt = raw?.lastActiveAt;
};
return prepareBaseRecord({
action,
database,
tableName: SERVERS,
value,
fieldsMapper,
});
};

View File

@@ -0,0 +1,85 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {
transformInfoRecord,
transformGlobalRecord,
transformServersRecord,
} from '@database/operator/app_data_operator/transformers/index';
import {OperationType} from '@typings/database/enums';
describe('** APP DATA TRANSFORMER **', () => {
beforeAll(async () => {
await DatabaseManager.init([]);
});
it('=> transformServersRecord: should return an array of type Servers', async () => {
expect.assertions(3);
const database = DatabaseManager.appDatabase?.database;
expect(database).toBeTruthy();
const preparedRecords = await transformServersRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
db_path: 'mm-server',
display_name: 's-displayName',
mention_count: 1,
unread_count: 0,
url: 'https://community.mattermost.com',
isSecured: true,
lastActiveAt: 1623926359,
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Servers');
});
it('=> transformInfoRecord: should return an array of type Info', async () => {
expect.assertions(3);
const database = DatabaseManager.appDatabase?.database;
expect(database).toBeTruthy();
const preparedRecords = await transformInfoRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
build_number: 'build-7',
created_at: 1,
version_number: 'v-1',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Info');
});
it('=> transformGlobalRecord: should return an array of type Global', async () => {
expect.assertions(3);
const database = DatabaseManager.appDatabase?.database;
expect(database).toBeTruthy();
const preparedRecords = await transformGlobalRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {name: 'g-n1', value: 'g-v1'},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Global');
});
});

View File

@@ -0,0 +1,239 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database, Q} from '@nozbe/watermelondb';
import DataOperatorException from '@database/exceptions/data_operator_exception';
import {
getRangeOfValues,
getValidRecordsForUpdate,
retrieveRecords,
} from '@database/operator/utils/general';
import type Model from '@nozbe/watermelondb/Model';
import type {
HandleRecordsArgs,
OperationArgs,
ProcessRecordResults,
ProcessRecordsArgs,
RawValue,
RecordPair,
} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
export interface BaseDataOperatorType {
database: Database;
handleRecords: ({findMatchingRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues, tableName, prepareRecordsOnly}: HandleRecordsArgs) => Promise<Model[]>;
processRecords: ({createOrUpdateRawValues, deleteRawValues, tableName, findMatchingRecordBy, fieldName}: ProcessRecordsArgs) => Promise<ProcessRecordResults>;
batchRecords: (models: Model[]) => Promise<void>;
prepareRecords: ({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs) => Promise<Model[]>;
}
export default class BaseDataOperator {
database: Database;
constructor(database: Database) {
this.database = database;
}
/**
* processRecords: This method weeds out duplicates entries. It may happen that we do multiple inserts for
* the same value. Hence, prior to that we query the database and pick only those values that are 'new' from the 'Raw' array.
* @param {ProcessRecordsArgs} inputsArg
* @param {RawValue[]} inputsArg.createOrUpdateRawValues
* @param {string} inputsArg.tableName
* @param {string} inputsArg.fieldName
* @param {(existing: Model, newElement: RawValue) => boolean} inputsArg.findMatchingRecordBy
* @returns {Promise<{ProcessRecordResults}>}
*/
processRecords = async ({createOrUpdateRawValues, deleteRawValues = [], tableName, findMatchingRecordBy, fieldName}: ProcessRecordsArgs): Promise<ProcessRecordResults> => {
const getRecords = async (rawValues : RawValue[]) => {
// We will query a table where one of its fields can match a range of values. Hence, here we are extracting all those potential values.
const columnValues: string[] = getRangeOfValues({
fieldName,
raws: rawValues,
});
if (!columnValues.length && rawValues.length) {
throw new DataOperatorException(
`Invalid "fieldName" or "tableName" has been passed to the processRecords method for tableName ${tableName} fieldName ${fieldName}`,
);
}
if (!rawValues.length) {
return [];
}
const existingRecords = await retrieveRecords({
database: this.database,
tableName,
condition: Q.where(fieldName, Q.oneOf(columnValues)),
});
return existingRecords;
};
const createRaws: RecordPair[] = [];
const updateRaws: RecordPair[] = [];
// for delete flow
const deleteRaws = await getRecords(deleteRawValues);
// for create or update flow
const createOrUpdateRaws = await getRecords(createOrUpdateRawValues);
if (createOrUpdateRawValues.length > 0) {
createOrUpdateRawValues.forEach((newElement: RawValue) => {
const findIndex = createOrUpdateRaws.findIndex((existing) => {
return findMatchingRecordBy(existing, newElement);
});
// We found a record in the database that matches this element; hence, we'll proceed for an UPDATE operation
if (findIndex > -1) {
const existingRecord = createOrUpdateRaws[findIndex];
// Some raw value has an update_at field. We'll proceed to update only if the update_at value is different from the record's value in database
const updateRecords = getValidRecordsForUpdate({
tableName,
existingRecord,
newValue: newElement,
});
updateRaws.push(updateRecords);
return;
}
// This RawValue is not present in the database; hence, we need to create it
createRaws.push({record: undefined, raw: newElement});
});
}
return {
createRaws,
updateRaws,
deleteRaws,
};
};
/**
* prepareRecords: Utility method that actually calls the operators for the handlers
* @param {OperationArgs} prepareRecord
* @param {string} prepareRecord.tableName
* @param {RawValue[]} prepareRecord.createRaws
* @param {RawValue[]} prepareRecord.updateRaws
* @param {Model[]} prepareRecord.deleteRaws
* @param {(TransformerArgs) => Promise<Model>;} prepareRecord.composer
* @throws {DataOperatorException}
* @returns {Promise<Model[]>}
*/
prepareRecords = async ({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs) => {
if (!this.database) {
throw new DataOperatorException('Database not defined');
}
let preparedRecords: Promise<Model>[] = [];
// create operation
if (createRaws?.length) {
const recordPromises = createRaws.map(
(createRecord: RecordPair) => {
return transformer({
database: this.database,
tableName,
value: createRecord,
action: OperationType.CREATE,
});
},
);
preparedRecords = preparedRecords.concat(recordPromises);
}
// update operation
if (updateRaws?.length) {
const recordPromises = updateRaws.map(
(updateRecord: RecordPair) => {
return transformer({
database: this.database,
tableName,
value: updateRecord,
action: OperationType.UPDATE,
});
},
);
preparedRecords = preparedRecords.concat(recordPromises);
}
const results = await Promise.all(preparedRecords);
if (deleteRaws?.length) {
deleteRaws.forEach((deleteRecord) => {
results.push(deleteRecord.prepareDestroyPermanently());
});
}
return results;
};
/**
* batchRecords: Accepts an instance of Database (either Default or Server) and an array of
* prepareCreate/prepareUpdate 'models' and executes the actions on the database.
* @param {Array} models
* @throws {DataOperatorException}
* @returns {Promise<void>}
*/
batchRecords = async (models: Model[]) => {
try {
if (models.length > 0) {
await this.database.action(async () => {
await this.database.batch(...models);
});
}
} catch (e) {
throw new DataOperatorException('batchRecords error ', e);
}
};
/**
* handleRecords : Utility that processes some records' data against values already present in the database so as to avoid duplicity.
* @param {HandleRecordsArgs} handleRecordsArgs
* @param {(existing: Model, newElement: RawValue) => boolean} handleRecordsArgs.findMatchingRecordBy
* @param {string} handleRecordsArgs.fieldName
* @param {(TransformerArgs) => Promise<Model>} handleRecordsArgs.composer
* @param {RawValue[]} handleRecordsArgs.createOrUpdateRawValues
* @param {RawValue[]} handleRecordsArgs.deleteRawValues
* @param {string} handleRecordsArgs.tableName
* @returns {Promise<Model[]>}
*/
handleRecords = async ({findMatchingRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues = [], tableName, prepareRecordsOnly = true}: HandleRecordsArgs) => {
if (!createOrUpdateRawValues.length) {
throw new DataOperatorException(
`An empty "rawValues" array has been passed to the handleRecords method for tableName ${tableName}`,
);
}
const {createRaws, deleteRaws, updateRaws} = await this.processRecords({
createOrUpdateRawValues,
deleteRawValues,
tableName,
findMatchingRecordBy,
fieldName,
});
let models: Model[] = [];
models = await this.prepareRecords({
tableName,
createRaws,
updateRaws,
deleteRaws,
transformer,
});
if (!prepareRecordsOnly && models?.length) {
await this.batchRecords(models);
}
return models;
};
}

View File

@@ -1,324 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DataOperatorException from '@database/exceptions/data_operator_exception';
import DatabaseManager from '@database/manager';
import Operator from '@database/operator';
import {
isRecordAppEqualToRaw,
isRecordCustomEmojiEqualToRaw,
isRecordGlobalEqualToRaw,
isRecordRoleEqualToRaw,
isRecordServerEqualToRaw,
isRecordSystemEqualToRaw,
isRecordTermsOfServiceEqualToRaw,
} from '@database/operator/comparators';
import {
prepareAppRecord,
prepareCustomEmojiRecord,
prepareGlobalRecord,
prepareRoleRecord,
prepareServersRecord,
prepareSystemRecord,
prepareTermsOfServiceRecord,
} from '@database/operator/prepareRecords/general';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {RawGlobal, RawRole, RawServers, RawTermsOfService} from '@typings/database/database';
import {DatabaseType, IsolatedEntities} from '@typings/database/enums';
jest.mock('@database/manager');
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** DataOperator: Base Handlers tests ***', () => {
let databaseManagerClient: DatabaseManager;
let operatorClient: Operator;
beforeAll(async () => {
databaseManagerClient = new DatabaseManager();
const database = await databaseManagerClient.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
dbName: 'base_handler',
dbType: DatabaseType.SERVER,
serverUrl: 'baseHandler.test.com',
},
});
operatorClient = new Operator(database!);
});
it('=> HandleApp: should write to APP entity', async () => {
expect.assertions(3);
const defaultDatabase = await databaseManagerClient.getDefaultDatabase();
expect(defaultDatabase).toBeTruthy();
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await operatorClient.handleIsolatedEntity({
tableName: IsolatedEntities.APP,
values: [
{
build_number: 'build-10x',
created_at: 1,
version_number: 'version-10',
},
{
build_number: 'build-11y',
created_at: 1,
version_number: 'version-11',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'version_number',
operator: prepareAppRecord,
findMatchingRecordBy: isRecordAppEqualToRaw,
rawValues: [
{
build_number: 'build-10x',
created_at: 1,
version_number: 'version-10',
},
{
build_number: 'build-11y',
created_at: 1,
version_number: 'version-11',
},
],
tableName: 'app',
prepareRecordsOnly: false,
});
});
it('=> HandleGlobal: should write to GLOBAL entity', async () => {
expect.assertions(2);
const defaultDatabase = await databaseManagerClient.getDefaultDatabase();
expect(defaultDatabase).toBeTruthy();
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
const values: RawGlobal[] = [{name: 'global-1-name', value: 'global-1-value'}];
await operatorClient.handleIsolatedEntity({
tableName: IsolatedEntities.GLOBAL,
values,
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
findMatchingRecordBy: isRecordGlobalEqualToRaw,
fieldName: 'name',
operator: prepareGlobalRecord,
rawValues: values,
tableName: 'global',
prepareRecordsOnly: false,
});
});
it('=> HandleServers: should write to SERVERS entity', async () => {
expect.assertions(2);
const defaultDatabase = await databaseManagerClient.getDefaultDatabase();
expect(defaultDatabase).toBeTruthy();
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
const values: RawServers[] = [
{
db_path: 'server.db',
display_name: 'community',
mention_count: 0,
unread_count: 0,
url: 'https://community.mattermost.com',
isSecured: true,
lastActiveAt: 1623926359,
},
];
await operatorClient.handleIsolatedEntity({
tableName: IsolatedEntities.SERVERS,
values,
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'url',
operator: prepareServersRecord,
findMatchingRecordBy: isRecordServerEqualToRaw,
rawValues: [
{
db_path: 'server.db',
display_name: 'community',
mention_count: 0,
unread_count: 0,
url: 'https://community.mattermost.com',
isSecured: true,
lastActiveAt: 1623926359,
},
],
tableName: 'servers',
prepareRecordsOnly: false,
});
});
it('=> HandleRole: should write to ROLE entity', async () => {
expect.assertions(1);
await createTestConnection({databaseName: 'base_handler', setActive: true});
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
const values: RawRole[] = [
{
id: 'custom-emoji-id-1',
name: 'custom-emoji-1',
permissions: ['custom-emoji-1'],
},
];
await operatorClient.handleIsolatedEntity({
tableName: IsolatedEntities.ROLE,
values,
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'id',
operator: prepareRoleRecord,
findMatchingRecordBy: isRecordRoleEqualToRaw,
rawValues: [
{
id: 'custom-emoji-id-1',
name: 'custom-emoji-1',
permissions: ['custom-emoji-1'],
},
],
tableName: 'Role',
prepareRecordsOnly: false,
});
});
it('=> HandleCustomEmojis: should write to CUSTOM_EMOJI entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'base_handler', setActive: true});
await operatorClient.handleIsolatedEntity({
tableName: IsolatedEntities.CUSTOM_EMOJI,
values: [
{
id: 'i',
create_at: 1580913641769,
update_at: 1580913641769,
delete_at: 0,
creator_id: '4cprpki7ri81mbx8efixcsb8jo',
name: 'boomI',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'id',
rawValues: [
{
id: 'i',
create_at: 1580913641769,
update_at: 1580913641769,
delete_at: 0,
creator_id: '4cprpki7ri81mbx8efixcsb8jo',
name: 'boomI',
},
],
tableName: 'CustomEmoji',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordCustomEmojiEqualToRaw,
operator: prepareCustomEmojiRecord,
});
});
it('=> HandleSystem: should write to SYSTEM entity', async () => {
expect.assertions(1);
await createTestConnection({databaseName: 'base_handler', setActive: true});
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
const values = [{id: 'system-id-1', name: 'system-1', value: 'system-1'}];
await operatorClient.handleIsolatedEntity({
tableName: IsolatedEntities.SYSTEM,
values,
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
findMatchingRecordBy: isRecordSystemEqualToRaw,
fieldName: 'name',
operator: prepareSystemRecord,
rawValues: values,
tableName: 'System',
prepareRecordsOnly: false,
});
});
it('=> HandleTermsOfService: should write to TERMS_OF_SERVICE entity', async () => {
expect.assertions(1);
await createTestConnection({databaseName: 'base_handler', setActive: true});
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
const values: RawTermsOfService[] = [
{
id: 'tos-1',
accepted_at: 1,
create_at: 1613667352029,
user_id: 'user1613667352029',
text: '',
},
];
await operatorClient.handleIsolatedEntity({
tableName: IsolatedEntities.TERMS_OF_SERVICE,
values,
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
findMatchingRecordBy: isRecordTermsOfServiceEqualToRaw,
fieldName: 'id',
operator: prepareTermsOfServiceRecord,
rawValues: values,
tableName: 'TermsOfService',
prepareRecordsOnly: false,
});
});
it('=> No table name: should not call executeInDatabase if tableName is invalid', async () => {
expect.assertions(2);
const defaultDatabase = await databaseManagerClient.getDefaultDatabase();
expect(defaultDatabase).toBeTruthy();
await expect(
operatorClient.handleIsolatedEntity({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
tableName: 'INVALID_TABLE_NAME',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
values: [{id: 'tos-1', accepted_at: 1}],
}),
).rejects.toThrow(DataOperatorException);
});
});

View File

@@ -1,448 +0,0 @@
// 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 DataOperatorException from '@database/exceptions/data_operator_exception';
import DatabaseConnectionException from '@database/exceptions/database_connection_exception';
import DatabaseManager from '@database/manager';
import {
isRecordAppEqualToRaw,
isRecordCustomEmojiEqualToRaw,
isRecordGlobalEqualToRaw,
isRecordRoleEqualToRaw,
isRecordServerEqualToRaw,
isRecordSystemEqualToRaw,
isRecordTermsOfServiceEqualToRaw,
} from '@database/operator/comparators';
import {
prepareAppRecord,
prepareCustomEmojiRecord,
prepareGlobalRecord,
prepareRoleRecord,
prepareServersRecord,
prepareSystemRecord,
prepareTermsOfServiceRecord,
} from '@database/operator/prepareRecords/general';
import {
getRangeOfValues,
getRawRecordPairs,
getUniqueRawsBy,
getValidRecordsForUpdate,
retrieveRecords,
} from '@database/operator/utils/general';
import {
BatchOperationsArgs,
DatabaseInstance,
HandleEntityRecordsArgs,
HandleIsolatedEntityArgs,
PrepareForDatabaseArgs,
PrepareRecordsArgs,
ProcessInputsArgs,
RawValue,
RecordPair,
} from '@typings/database/database';
import {IsolatedEntities, OperationType} from '@typings/database/enums';
export interface BaseHandlerMix {
activeDatabase: Database;
getActiveDatabase: () => DatabaseInstance;
setActiveDatabase: (database: Database) => void;
handleIsolatedEntity: ({tableName, values, prepareRecordsOnly}: HandleIsolatedEntityArgs) => Model[];
handleEntityRecords: ({findMatchingRecordBy, fieldName, operator, rawValues, tableName, prepareRecordsOnly}: HandleEntityRecordsArgs) => Promise<null | Model[]>;
processInputs: ({rawValues, tableName, findMatchingRecordBy, fieldName}: ProcessInputsArgs) => Promise<{ createRaws: RecordPair[]; updateRaws: RecordPair[] }>;
batchOperations: ({database, models}: BatchOperationsArgs) => Promise<void>;
prepareRecords: ({database, tableName, createRaws, updateRaws, recordOperator}: PrepareRecordsArgs) => Promise<Model[]>;
executeInDatabase: ({createRaws, recordOperator, tableName, updateRaws}: PrepareForDatabaseArgs) => Promise<void>;
getDatabase: (tableName: string) => Database;
getDefaultDatabase: () => Promise<Database>;
getServerDatabase: () => Promise<Database>;
}
class BaseHandler {
/**
* activeDatabase : 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}
*/
activeDatabase: DatabaseInstance;
constructor(serverDatabase: Database) {
this.activeDatabase = serverDatabase;
}
/**
* getActiveDatabase : getter for the activeDatabase
* @returns {DatabaseInstance}
*/
getActiveDatabase = () => this.activeDatabase;
/**
* setActiveDatabase: setter for the activeDatabase
* @param {} database
*/
setActiveDatabase = (database: Database) => {
this.activeDatabase = database;
};
/**
* handleIsolatedEntity: Handler responsible for the Create/Update operations on the isolated entities as described
* by the IsolatedEntities enum
* @param {HandleIsolatedEntityArgs} isolatedEntityArgs
* @param {IsolatedEntities} isolatedEntityArgs.tableName
* @param {boolean} isolatedEntityArgs.prepareRecordsOnly
* @param {RawValue} isolatedEntityArgs.values
* @throws DataOperatorException
* @returns {Model[]}
*/
handleIsolatedEntity = async ({tableName, values, prepareRecordsOnly = true}: HandleIsolatedEntityArgs) => {
let findMatchingRecordBy;
let fieldName;
let operator;
let rawValues;
let records: Model[] = [];
if (!values.length) {
throw new DataOperatorException(
`An empty "values" array has been passed to the handleIsolatedEntity method for entity ${tableName}`,
);
}
switch (tableName) {
case IsolatedEntities.APP: {
findMatchingRecordBy = isRecordAppEqualToRaw;
fieldName = 'version_number';
operator = prepareAppRecord;
rawValues = getUniqueRawsBy({raws: values, key: 'version_number'});
break;
}
case IsolatedEntities.CUSTOM_EMOJI: {
findMatchingRecordBy = isRecordCustomEmojiEqualToRaw;
fieldName = 'id';
operator = prepareCustomEmojiRecord;
rawValues = getUniqueRawsBy({raws: values, key: 'id'});
break;
}
case IsolatedEntities.GLOBAL: {
findMatchingRecordBy = isRecordGlobalEqualToRaw;
fieldName = 'name';
operator = prepareGlobalRecord;
rawValues = getUniqueRawsBy({raws: values, key: 'name'});
break;
}
case IsolatedEntities.ROLE: {
findMatchingRecordBy = isRecordRoleEqualToRaw;
fieldName = 'id';
operator = prepareRoleRecord;
rawValues = getUniqueRawsBy({raws: values, key: 'id'});
break;
}
case IsolatedEntities.SERVERS: {
findMatchingRecordBy = isRecordServerEqualToRaw;
fieldName = 'url';
operator = prepareServersRecord;
rawValues = getUniqueRawsBy({raws: values, key: 'display_name'});
break;
}
case IsolatedEntities.SYSTEM: {
findMatchingRecordBy = isRecordSystemEqualToRaw;
fieldName = 'name';
operator = prepareSystemRecord;
rawValues = getUniqueRawsBy({raws: values, key: 'name'});
break;
}
case IsolatedEntities.TERMS_OF_SERVICE: {
findMatchingRecordBy = isRecordTermsOfServiceEqualToRaw;
fieldName = 'id';
operator = prepareTermsOfServiceRecord;
rawValues = getUniqueRawsBy({raws: values, key: 'id'});
break;
}
default: {
throw new DataOperatorException(
`handleIsolatedEntity was called with an invalid table name ${tableName}`,
);
}
}
if (fieldName && findMatchingRecordBy) {
records = await this.handleEntityRecords({
fieldName,
findMatchingRecordBy,
operator,
prepareRecordsOnly,
rawValues,
tableName,
});
return records;
}
return records;
};
/**
* handleEntityRecords : Utility that processes some entities' data against values already present in the database so as to avoid duplicity.
* @param {HandleEntityRecordsArgs} handleEntityArgs
* @param {(existing: Model, newElement: RawValue) => boolean} handleEntityArgs.findMatchingRecordBy
* @param {string} handleEntityArgs.fieldName
* @param {(DataFactoryArgs) => Promise<Model>} handleEntityArgs.operator
* @param {RawValue[]} handleEntityArgs.rawValues
* @param {string} handleEntityArgs.tableName
* @returns {Promise<Model[]>}
*/
handleEntityRecords = async ({findMatchingRecordBy, fieldName, operator, rawValues, tableName, prepareRecordsOnly = true}: HandleEntityRecordsArgs) => {
if (!rawValues.length) {
throw new DataOperatorException(
`An empty "rawValues" array has been passed to the handleEntityRecords method for tableName ${tableName}`,
);
}
const {createRaws, updateRaws} = await this.processInputs({
rawValues,
tableName,
findMatchingRecordBy,
fieldName,
});
const database = await this.getDatabase(tableName);
let models: Model[] = [];
models = await this.prepareRecords({
database,
tableName,
createRaws,
updateRaws,
recordOperator: operator,
});
if (prepareRecordsOnly) {
return models;
}
if (models?.length > 0) {
await this.batchOperations({database, models});
}
return models;
};
/**
* processInputs: This method weeds out duplicates entries. It may happen that we do multiple inserts for
* the same value. Hence, prior to that we query the database and pick only those values that are 'new' from the 'Raw' array.
* @param {ProcessInputsArgs} inputsArg
* @param {RawValue[]} inputsArg.rawValues
* @param {string} inputsArg.tableName
* @param {string} inputsArg.fieldName
* @param {(existing: Model, newElement: RawValue) => boolean} inputsArg.findMatchingRecordBy
* @returns {Promise<{createRaws: RecordPair[], updateRaws: RecordPair[]} | {createRaws: RecordPair[], updateRaws: RecordPair[]}>}
*/
processInputs = async ({rawValues, tableName, findMatchingRecordBy, fieldName}: ProcessInputsArgs) => {
// We will query an entity where one of its fields can match a range of values. Hence, here we are extracting all those potential values.
const columnValues: string[] = getRangeOfValues({
fieldName,
raws: rawValues,
});
const database = await this.getDatabase(tableName);
const existingRecords = await retrieveRecords({
database,
tableName,
condition: Q.where(fieldName, Q.oneOf(columnValues)),
});
const createRaws: RecordPair[] = [];
const updateRaws: RecordPair[] = [];
if (existingRecords.length > 0) {
rawValues.forEach((newElement: RawValue) => {
const findIndex = existingRecords.findIndex((existing) => {
return findMatchingRecordBy(existing, newElement);
});
// We found a record in the database that matches this element; hence, we'll proceed for an UPDATE operation
if (findIndex > -1) {
const existingRecord = existingRecords[findIndex];
// Some raw value has an update_at field. We'll proceed to update only if the update_at value is different from the record's value in database
const updateRecords = getValidRecordsForUpdate({
tableName,
existingRecord,
newValue: newElement,
});
return updateRaws.push(updateRecords);
}
// This RawValue is not present in the database; hence, we need to create it
return createRaws.push({record: undefined, raw: newElement});
});
return {
createRaws,
updateRaws,
};
}
return {
createRaws: getRawRecordPairs(rawValues),
updateRaws,
};
};
/**
* 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 {BatchOperationsArgs} operation
* @param {Database} operation.database
* @param {Array} operation.models
* @throws {DataOperatorException}
* @returns {Promise<void>}
*/
batchOperations = async ({database, models}: BatchOperationsArgs) => {
try {
if (models.length > 0) {
await database.action(async () => {
await database.batch(...models);
});
}
} catch (e) {
throw new DataOperatorException('batchOperations error ', e);
}
};
/**
* prepareRecords: Utility method that actually calls the operators for the handlers
* @param {PrepareRecordsArgs} prepareRecord
* @param {Database} prepareRecord.database
* @param {string} prepareRecord.tableName
* @param {RawValue[]} prepareRecord.createRaws
* @param {RawValue[]} prepareRecord.updateRaws
* @param {(DataFactoryArgs) => Promise<Model>;} prepareRecord.recordOperator
* @throws {DataOperatorException}
* @returns {Promise<Model[]>}
*/
prepareRecords = async ({database, tableName, createRaws, updateRaws, recordOperator}: PrepareRecordsArgs) => {
if (!database) {
throw new DataOperatorException(
'prepareRecords accepts only rawPosts of type RawValue[] or valid database connection',
);
}
let preparedRecords: Promise<Model>[] = [];
// create operation
if (createRaws?.length) {
const recordPromises = createRaws.map(
(createRecord: RecordPair) => {
return recordOperator({
database,
tableName,
value: createRecord,
action: OperationType.CREATE,
});
},
);
preparedRecords = preparedRecords.concat(recordPromises);
}
// update operation
if (updateRaws?.length) {
const recordPromises = updateRaws.map(
(updateRecord: RecordPair) => {
return recordOperator({
database,
tableName,
value: updateRecord,
action: OperationType.UPDATE,
});
},
);
preparedRecords = preparedRecords.concat(recordPromises);
}
const results = await Promise.all(preparedRecords);
return results;
};
/**
* executeInDatabase: Handles the Create/Update operations on an entity.
* @param {PrepareForDatabaseArgs} executeInDatabase
* @param {string} executeInDatabase.tableName
* @param {RecordValue[]} executeInDatabase.createRaws
* @param {RecordValue[]} executeInDatabase.updateRaws
* @param {(DataFactoryArgs) => Promise<Model>} executeInDatabase.recordOperator
* @returns {Promise<void>}
*/
executeInDatabase = async ({createRaws, recordOperator, tableName, updateRaws}: PrepareForDatabaseArgs) => {
const database = await this.getDatabase(tableName);
const models = await this.prepareRecords({
database,
tableName,
createRaws,
updateRaws,
recordOperator,
});
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>}
*/
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
* @throws {DatabaseConnectionException}
* @returns {Promise<Database>}
*/
getDefaultDatabase = async () => {
const databaseManagerClient = new DatabaseManager();
const connection = await databaseManagerClient.getDefaultDatabase();
if (connection === undefined) {
throw new DatabaseConnectionException(
'An error occurred while retrieving the default database',
'',
);
}
return connection;
};
/**
* getServerDatabase: Returns the current active server database (multi-server support)
* @throws {DatabaseConnectionException}
* @returns {Promise<Database>}
*/
getServerDatabase = async () => {
// Third parties trying to update the database
if (this.activeDatabase) {
return this.activeDatabase;
}
throw new DatabaseConnectionException(
"This operator client didn't have its activeDatabase set",
'',
);
};
}
export default BaseHandler;

View File

@@ -1,240 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import Operator from '@database/operator';
import {
isRecordChannelEqualToRaw,
isRecordChannelInfoEqualToRaw,
isRecordMyChannelEqualToRaw,
isRecordMyChannelSettingsEqualToRaw,
} from '@database/operator/comparators';
import {
prepareChannelInfoRecord,
prepareChannelRecord,
prepareMyChannelRecord,
prepareMyChannelSettingsRecord,
} from '@database/operator/prepareRecords/channel';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {DatabaseType} from '@typings/database/enums';
jest.mock('@database/manager');
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** Operator: Channel Handlers tests ***', () => {
let databaseManagerClient: DatabaseManager;
let operatorClient: Operator;
beforeAll(async () => {
databaseManagerClient = new DatabaseManager();
const database = await databaseManagerClient.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
dbName: 'base_handler',
dbType: DatabaseType.SERVER,
serverUrl: 'baseHandler.test.com',
},
});
operatorClient = new Operator(database!);
});
it('=> HandleChannel: should write to CHANNEL entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'channel_handler', setActive: true});
await operatorClient.handleChannel({
channels: [
{
id: 'kjlw9j1ttnxwig7tnqgebg7dtipno',
create_at: 1600185541285,
update_at: 1604401077256,
delete_at: 0,
team_id: '',
type: 'D',
display_name: '',
name: 'gh781zkzkhh357b4bejephjz5u8daw__9ciscaqbrpd6d8s68k76xb9bte',
header: '(https://mattermost',
purpose: '',
last_post_at: 1617311494451,
total_msg_count: 585,
extra_update_at: 0,
creator_id: '',
scheme_id: null,
props: null,
group_constrained: null,
shared: null,
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'id',
rawValues: [
{
id: 'kjlw9j1ttnxwig7tnqgebg7dtipno',
create_at: 1600185541285,
update_at: 1604401077256,
delete_at: 0,
team_id: '',
type: 'D',
display_name: '',
name: 'gh781zkzkhh357b4bejephjz5u8daw__9ciscaqbrpd6d8s68k76xb9bte',
header: '(https://mattermost',
purpose: '',
last_post_at: 1617311494451,
total_msg_count: 585,
extra_update_at: 0,
creator_id: '',
scheme_id: null,
props: null,
group_constrained: null,
shared: null,
},
],
tableName: 'Channel',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordChannelEqualToRaw,
operator: prepareChannelRecord,
});
});
it('=> HandleMyChannelSettings: should write to MY_CHANNEL_SETTINGS entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'channel_handler', setActive: true});
await operatorClient.handleMyChannelSettings({
settings: [
{
channel_id: 'c',
notify_props: {
desktop: 'all',
desktop_sound: true,
email: true,
first_name: true,
mention_keys: '',
push: 'mention',
channel: true,
},
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'channel_id',
rawValues: [
{
channel_id: 'c',
notify_props: {
desktop: 'all',
desktop_sound: true,
email: true,
first_name: true,
mention_keys: '',
push: 'mention',
channel: true,
},
},
],
tableName: 'MyChannelSettings',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordMyChannelSettingsEqualToRaw,
operator: prepareMyChannelSettingsRecord,
});
});
it('=> HandleChannelInfo: should write to CHANNEL_INFO entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'channel_handler', setActive: true});
await operatorClient.handleChannelInfo({
channelInfos: [
{
channel_id: 'c',
guest_count: 10,
header: 'channel info header',
member_count: 10,
pinned_post_count: 3,
purpose: 'sample channel ',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'channel_id',
rawValues: [
{
channel_id: 'c',
guest_count: 10,
header: 'channel info header',
member_count: 10,
pinned_post_count: 3,
purpose: 'sample channel ',
},
],
tableName: 'ChannelInfo',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordChannelInfoEqualToRaw,
operator: prepareChannelInfoRecord,
});
});
it('=> HandleMyChannel: should write to MY_CHANNEL entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'channel_handler', setActive: true});
await operatorClient.handleMyChannel({
myChannels: [
{
channel_id: 'c',
last_post_at: 1617311494451,
last_viewed_at: 1617311494451,
mentions_count: 3,
message_count: 10,
roles: 'guest',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'channel_id',
rawValues: [
{
channel_id: 'c',
last_post_at: 1617311494451,
last_viewed_at: 1617311494451,
mentions_count: 3,
message_count: 10,
roles: 'guest',
},
],
tableName: 'MyChannel',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordMyChannelEqualToRaw,
operator: prepareMyChannelRecord,
});
});
});

View File

@@ -1,225 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import Operator from '@database/operator';
import {
isRecordGroupEqualToRaw,
isRecordGroupMembershipEqualToRaw,
isRecordGroupsInChannelEqualToRaw,
isRecordGroupsInTeamEqualToRaw,
} from '@database/operator/comparators';
import {
prepareGroupMembershipRecord,
prepareGroupRecord,
prepareGroupsInChannelRecord,
prepareGroupsInTeamRecord,
} from '@database/operator/prepareRecords/group';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {DatabaseType} from '@typings/database/enums';
jest.mock('@database/manager');
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** Operator: Group Handlers tests ***', () => {
let databaseManagerClient: DatabaseManager;
let operatorClient: Operator;
beforeAll(async () => {
databaseManagerClient = new DatabaseManager();
const database = await databaseManagerClient.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
dbName: 'base_handler',
dbType: DatabaseType.SERVER,
serverUrl: 'baseHandler.test.com',
},
});
operatorClient = new Operator(database!);
});
it('=> HandleGroup: should write to GROUP entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'group_handler', setActive: true});
await operatorClient.handleGroup({
groups: [
{
id: 'id_groupdfjdlfkjdkfdsf',
name: 'mobile_team',
display_name: 'mobile team',
description: '',
source: '',
remote_id: '',
create_at: 0,
update_at: 0,
delete_at: 0,
has_syncables: true,
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'name',
rawValues: [
{
id: 'id_groupdfjdlfkjdkfdsf',
name: 'mobile_team',
display_name: 'mobile team',
description: '',
source: '',
remote_id: '',
create_at: 0,
update_at: 0,
delete_at: 0,
has_syncables: true,
},
],
tableName: 'Group',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupEqualToRaw,
operator: prepareGroupRecord,
});
});
it('=> HandleGroupsInTeam: should write to GROUPS_IN_TEAM entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'group_handler', setActive: true});
await operatorClient.handleGroupsInTeam({
groupsInTeams: [
{
team_id: 'team_899',
team_display_name: '',
team_type: '',
group_id: 'group_id89',
auto_add: true,
create_at: 0,
delete_at: 0,
update_at: 0,
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'group_id',
rawValues: [
{
team_id: 'team_899',
team_display_name: '',
team_type: '',
group_id: 'group_id89',
auto_add: true,
create_at: 0,
delete_at: 0,
update_at: 0,
},
],
tableName: 'GroupsInTeam',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupsInTeamEqualToRaw,
operator: prepareGroupsInTeamRecord,
});
});
it('=> HandleGroupsInChannel: should write to GROUPS_IN_CHANNEL entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'group_handler', setActive: true});
await operatorClient.handleGroupsInChannel({
groupsInChannels: [
{
auto_add: true,
channel_display_name: '',
channel_id: 'channelid',
channel_type: '',
create_at: 0,
delete_at: 0,
group_id: 'groupId',
team_display_name: '',
team_id: '',
team_type: '',
update_at: 0,
member_count: 0,
timezone_count: 0,
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'group_id',
rawValues: [
{
auto_add: true,
channel_display_name: '',
channel_id: 'channelid',
channel_type: '',
create_at: 0,
delete_at: 0,
group_id: 'groupId',
team_display_name: '',
team_id: '',
team_type: '',
update_at: 0,
member_count: 0,
timezone_count: 0,
},
],
tableName: 'GroupsInChannel',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupsInChannelEqualToRaw,
operator: prepareGroupsInChannelRecord,
});
});
it('=> HandleGroupMembership: should write to GROUP_MEMBERSHIP entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'group_handler', setActive: true});
await operatorClient.handleGroupMembership({
groupMemberships: [
{
user_id: 'u4cprpki7ri81mbx8efixcsb8jo',
group_id: 'g4cprpki7ri81mbx8efixcsb8jo',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
rawValues: [
{
user_id: 'u4cprpki7ri81mbx8efixcsb8jo',
group_id: 'g4cprpki7ri81mbx8efixcsb8jo',
},
],
tableName: 'GroupMembership',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupMembershipEqualToRaw,
operator: prepareGroupMembershipRecord,
});
});
});

View File

@@ -1,318 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import Operator from '@database/operator';
import {
isRecordMyTeamEqualToRaw,
isRecordSlashCommandEqualToRaw,
isRecordTeamChannelHistoryEqualToRaw,
isRecordTeamEqualToRaw,
isRecordTeamMembershipEqualToRaw,
isRecordTeamSearchHistoryEqualToRaw,
} from '@database/operator/comparators';
import {
prepareMyTeamRecord,
prepareSlashCommandRecord,
prepareTeamChannelHistoryRecord,
prepareTeamMembershipRecord,
prepareTeamRecord,
prepareTeamSearchHistoryRecord,
} from '@database/operator/prepareRecords/team';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {DatabaseType} from '@typings/database/enums';
jest.mock('@database/manager');
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** Operator: Team Handlers tests ***', () => {
let databaseManagerClient: DatabaseManager;
let operatorClient: Operator;
beforeAll(async () => {
databaseManagerClient = new DatabaseManager();
const database = await databaseManagerClient.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
dbName: 'base_handler',
dbType: DatabaseType.SERVER,
serverUrl: 'baseHandler.test.com',
},
});
operatorClient = new Operator(database!);
});
it('=> HandleTeam: should write to TEAM entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'team_handler', setActive: true});
await operatorClient.handleTeam({
teams: [
{
id: 'rcgiyftm7jyrxnmdfdfa1osd8zswby',
create_at: 1445538153952,
update_at: 1588876392150,
delete_at: 0,
display_name: 'Contributors',
name: 'core',
description: '',
email: '',
type: 'O',
company_name: '',
allowed_domains: '',
invite_id: 'codoy5s743rq5mk18i7u5dfdfksz7e',
allow_open_invite: true,
last_team_icon_update: 1525181587639,
scheme_id: 'hbwgrncq1pfcdkpotzidfdmarn95o',
group_constrained: null,
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'id',
rawValues: [
{
id: 'rcgiyftm7jyrxnmdfdfa1osd8zswby',
create_at: 1445538153952,
update_at: 1588876392150,
delete_at: 0,
display_name: 'Contributors',
name: 'core',
description: '',
email: '',
type: 'O',
company_name: '',
allowed_domains: '',
invite_id: 'codoy5s743rq5mk18i7u5dfdfksz7e',
allow_open_invite: true,
last_team_icon_update: 1525181587639,
scheme_id: 'hbwgrncq1pfcdkpotzidfdmarn95o',
group_constrained: null,
},
],
tableName: 'Team',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamEqualToRaw,
operator: prepareTeamRecord,
});
});
it('=> HandleTeamMemberships: should write to TEAM_MEMBERSHIP entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'team_handler', setActive: true});
await operatorClient.handleTeamMemberships({
teamMemberships: [
{
team_id: 'a',
user_id: 'ab',
roles: '3ngdqe1e7tfcbmam4qgnxp91bw',
delete_at: 0,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
rawValues: [
{
team_id: 'a',
user_id: 'ab',
roles: '3ngdqe1e7tfcbmam4qgnxp91bw',
delete_at: 0,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
],
tableName: 'TeamMembership',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamMembershipEqualToRaw,
operator: prepareTeamMembershipRecord,
});
});
it('=> HandleMyTeam: should write to MY_TEAM entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'team_handler', setActive: true});
await operatorClient.handleMyTeam({
myTeams: [
{
team_id: 'teamA',
roles: 'roleA, roleB, roleC',
is_unread: true,
mentions_count: 3,
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'team_id',
rawValues: [
{
team_id: 'teamA',
roles: 'roleA, roleB, roleC',
is_unread: true,
mentions_count: 3,
},
],
tableName: 'MyTeam',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordMyTeamEqualToRaw,
operator: prepareMyTeamRecord,
});
});
it('=> HandleTeamChannelHistory: should write to TEAM_CHANNEL_HISTORY entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'team_handler', setActive: true});
await operatorClient.handleTeamChannelHistory({
teamChannelHistories: [
{
team_id: 'a',
channel_ids: ['ca', 'cb'],
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'team_id',
rawValues: [{team_id: 'a', channel_ids: ['ca', 'cb']}],
tableName: 'TeamChannelHistory',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamChannelHistoryEqualToRaw,
operator: prepareTeamChannelHistoryRecord,
});
});
it('=> HandleTeamSearchHistory: should write to TEAM_SEARCH_HISTORY entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'team_handler', setActive: true});
await operatorClient.handleTeamSearchHistory({
teamSearchHistories: [
{
team_id: 'a',
term: 'termA',
display_term: 'termA',
created_at: 1445538153952,
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'team_id',
rawValues: [
{
team_id: 'a',
term: 'termA',
display_term: 'termA',
created_at: 1445538153952,
},
],
tableName: 'TeamSearchHistory',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamSearchHistoryEqualToRaw,
operator: prepareTeamSearchHistoryRecord,
});
});
it('=> HandleSlashCommand: should write to SLASH_COMMAND entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'team_handler', setActive: true});
await operatorClient.handleSlashCommand({
slashCommands: [
{
id: 'command_1',
auto_complete: true,
auto_complete_desc: 'mock_command',
auto_complete_hint: 'hint',
create_at: 1445538153952,
creator_id: 'creator_id',
delete_at: 1445538153952,
description: 'description',
display_name: 'display_name',
icon_url: 'display_name',
method: 'get',
team_id: 'teamA',
token: 'token',
trigger: 'trigger',
update_at: 1445538153953,
url: 'url',
username: 'userA',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'id',
rawValues: [
{
id: 'command_1',
auto_complete: true,
auto_complete_desc: 'mock_command',
auto_complete_hint: 'hint',
create_at: 1445538153952,
creator_id: 'creator_id',
delete_at: 1445538153952,
description: 'description',
display_name: 'display_name',
icon_url: 'display_name',
method: 'get',
team_id: 'teamA',
token: 'token',
trigger: 'trigger',
update_at: 1445538153953,
url: 'url',
username: 'userA',
},
],
tableName: 'SlashCommand',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordSlashCommandEqualToRaw,
operator: prepareSlashCommandRecord,
});
});
});

View File

@@ -1,352 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import Operator from '@database/operator';
import {
isRecordChannelMembershipEqualToRaw,
isRecordPreferenceEqualToRaw,
isRecordUserEqualToRaw,
} from '@database/operator/comparators';
import {
prepareChannelMembershipRecord,
preparePreferenceRecord,
prepareUserRecord,
} from '@database/operator/prepareRecords/user';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {DatabaseType} from '@typings/database/enums';
jest.mock('@database/manager');
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** Operator: User Handlers tests ***', () => {
let databaseManagerClient: DatabaseManager;
let operatorClient: Operator;
beforeAll(async () => {
databaseManagerClient = new DatabaseManager();
const database = await databaseManagerClient.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
dbName: 'base_handler',
dbType: DatabaseType.SERVER,
serverUrl: 'baseHandler.test.com',
},
});
operatorClient = new Operator(database!);
});
it('=> HandleReactions: should write to both Reactions and CustomEmoji entities', async () => {
expect.assertions(2);
await createTestConnection({databaseName: 'user_handler', setActive: true});
const spyOnPrepareRecords = jest.spyOn(operatorClient as any, 'prepareRecords');
const spyOnBatchOperation = jest.spyOn(operatorClient as any, 'batchOperations');
await operatorClient.handleReactions({
reactions: [
{
create_at: 1608263728086,
delete_at: 0,
emoji_name: 'p4p1',
post_id: '4r9jmr7eqt8dxq3f9woypzurry',
update_at: 1608263728077,
user_id: 'ooumoqgq3bfiijzwbn8badznwc',
},
],
prepareRecordsOnly: false,
});
// Called twice: Once for Reaction record and once for CustomEmoji record
expect(spyOnPrepareRecords).toHaveBeenCalledTimes(2);
// Only one batch operation for both entities
expect(spyOnBatchOperation).toHaveBeenCalledTimes(1);
});
it('=> HandleUsers: should write to User entity', async () => {
expect.assertions(2);
const users = [
{
id: '9ciscaqbrpd6d8s68k76xb9bte',
create_at: 1599457495881,
update_at: 1607683720173,
delete_at: 0,
username: 'a.l',
auth_service: 'saml',
email: 'a.l@mattermost.com',
email_verified: true,
is_bot: false,
nickname: '',
first_name: 'A',
last_name: 'L',
position: 'Mobile Engineer',
roles: 'system_user',
props: {},
notify_props: {
desktop: 'all',
desktop_sound: true,
email: true,
first_name: true,
mention_keys: '',
push: 'mention',
channel: true,
auto_responder_active: false,
auto_responder_message: 'Hello, I am out of office and unable to respond to messages.',
comments: 'never',
desktop_notification_sound: 'Hello',
push_status: 'online',
},
last_password_update: 1604323112537,
last_picture_update: 1604686302260,
locale: 'en',
timezone: {
automaticTimezone: 'Indian/Mauritius',
manualTimezone: '',
useAutomaticTimezone: '',
},
},
];
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'user_handler', setActive: true});
await operatorClient.handleUsers({users, prepareRecordsOnly: false});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'id',
rawValues: [
{
id: '9ciscaqbrpd6d8s68k76xb9bte',
create_at: 1599457495881,
update_at: 1607683720173,
delete_at: 0,
username: 'a.l',
auth_service: 'saml',
email: 'a.l@mattermost.com',
email_verified: true,
is_bot: false,
nickname: '',
first_name: 'A',
last_name: 'L',
position: 'Mobile Engineer',
roles: 'system_user',
props: {},
notify_props: {
desktop: 'all',
desktop_sound: true,
email: true,
first_name: true,
mention_keys: '',
push: 'mention',
channel: true,
auto_responder_active: false,
auto_responder_message: 'Hello, I am out of office and unable to respond to messages.',
comments: 'never',
desktop_notification_sound: 'Hello',
push_status: 'online',
},
last_password_update: 1604323112537,
last_picture_update: 1604686302260,
locale: 'en',
timezone: {
automaticTimezone: 'Indian/Mauritius',
manualTimezone: '',
useAutomaticTimezone: '',
},
},
],
tableName: 'User',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordUserEqualToRaw,
operator: prepareUserRecord,
});
});
it('=> HandlePreferences: should write to PREFERENCE entity', async () => {
expect.assertions(2);
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'user_handler', setActive: true});
await operatorClient.handlePreferences({
preferences: [
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'group_channel_show',
name: 'qj91hepgjfn6xr4acm5xzd8zoc',
value: 'true',
},
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'notifications',
name: 'email_interval',
value: '30',
},
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'theme',
name: '',
value:
'{"awayIndicator":"#c1b966","buttonBg":"#4cbba4","buttonColor":"#ffffff","centerChannelBg":"#2f3e4e","centerChannelColor":"#dddddd","codeTheme":"solarized-dark","dndIndicator":"#e81023","errorTextColor":"#ff6461","image":"/static/files/0b8d56c39baf992e5e4c58d74fde0fd6.png","linkColor":"#a4ffeb","mentionBg":"#b74a4a","mentionColor":"#ffffff","mentionHighlightBg":"#984063","mentionHighlightLink":"#a4ffeb","newMessageSeparator":"#5de5da","onlineIndicator":"#65dcc8","sidebarBg":"#1b2c3e","sidebarHeaderBg":"#1b2c3e","sidebarHeaderTextColor":"#ffffff","sidebarText":"#ffffff","sidebarTextActiveBorder":"#66b9a7","sidebarTextActiveColor":"#ffffff","sidebarTextHoverBg":"#4a5664","sidebarUnreadText":"#ffffff","type":"Mattermost Dark"}',
},
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'tutorial_step',
name: '9ciscaqbrpd6d8s68k76xb9bte',
value: '2',
},
],
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
rawValues: [
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'group_channel_show',
name: 'qj91hepgjfn6xr4acm5xzd8zoc',
value: 'true',
},
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'notifications',
name: 'email_interval',
value: '30',
},
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'theme',
name: '',
value: '{"awayIndicator":"#c1b966","buttonBg":"#4cbba4","buttonColor":"#ffffff","centerChannelBg":"#2f3e4e","centerChannelColor":"#dddddd","codeTheme":"solarized-dark","dndIndicator":"#e81023","errorTextColor":"#ff6461","image":"/static/files/0b8d56c39baf992e5e4c58d74fde0fd6.png","linkColor":"#a4ffeb","mentionBg":"#b74a4a","mentionColor":"#ffffff","mentionHighlightBg":"#984063","mentionHighlightLink":"#a4ffeb","newMessageSeparator":"#5de5da","onlineIndicator":"#65dcc8","sidebarBg":"#1b2c3e","sidebarHeaderBg":"#1b2c3e","sidebarHeaderTextColor":"#ffffff","sidebarText":"#ffffff","sidebarTextActiveBorder":"#66b9a7","sidebarTextActiveColor":"#ffffff","sidebarTextHoverBg":"#4a5664","sidebarUnreadText":"#ffffff","type":"Mattermost Dark"}',
},
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'tutorial_step',
name: '9ciscaqbrpd6d8s68k76xb9bte',
value: '2',
},
],
tableName: 'Preference',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordPreferenceEqualToRaw,
operator: preparePreferenceRecord,
});
});
it('=> HandleChannelMembership: should write to CHANNEL_MEMBERSHIP entity', async () => {
expect.assertions(2);
const channelMemberships = [
{
channel_id: '17bfnb1uwb8epewp4q3x3rx9go',
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
roles: 'wqyby5r5pinxxdqhoaomtacdhc',
last_viewed_at: 1613667352029,
msg_count: 3864,
mention_count: 0,
notify_props: {
desktop: 'default',
email: 'default',
ignore_channel_mentions: 'default',
mark_unread: 'mention',
push: 'default',
},
last_update_at: 1613667352029,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
{
channel_id: '1yw6gxfr4bn1jbyp9nr7d53yew',
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
roles: 'channel_user',
last_viewed_at: 1615300540549,
msg_count: 16,
mention_count: 0,
notify_props: {
desktop: 'default',
email: 'default',
ignore_channel_mentions: 'default',
mark_unread: 'all',
push: 'default',
},
last_update_at: 1615300540549,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
];
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
await createTestConnection({databaseName: 'user_handler', setActive: true});
await operatorClient.handleChannelMembership({
channelMemberships,
prepareRecordsOnly: false,
});
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
rawValues: [
{
channel_id: '17bfnb1uwb8epewp4q3x3rx9go',
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
roles: 'wqyby5r5pinxxdqhoaomtacdhc',
last_viewed_at: 1613667352029,
msg_count: 3864,
mention_count: 0,
notify_props: {
desktop: 'default',
email: 'default',
ignore_channel_mentions: 'default',
mark_unread: 'mention',
push: 'default',
},
last_update_at: 1613667352029,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
{
channel_id: '1yw6gxfr4bn1jbyp9nr7d53yew',
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
roles: 'channel_user',
last_viewed_at: 1615300540549,
msg_count: 16,
mention_count: 0,
notify_props: {
desktop: 'default',
email: 'default',
ignore_channel_mentions: 'default',
mark_unread: 'all',
push: 'default',
},
last_update_at: 1615300540549,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
],
tableName: 'ChannelMembership',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordChannelMembershipEqualToRaw,
operator: prepareChannelMembershipRecord,
});
});
});

View File

@@ -1,26 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import BaseHandler, {BaseHandlerMix} from '@database/operator/handlers/base_handler';
import ChannelHandler, {ChannelHandlerMix} from '@database/operator/handlers/channel';
import GroupHandler, {GroupHandlerMix} from '@database/operator/handlers/group';
import PostHandler, {PostHandlerMix} from '@database/operator/handlers/post';
import TeamHandler, {TeamHandlerMix} from '@database/operator/handlers/team';
import UserHandler, {UserHandlerMix} from '@database/operator/handlers/user';
import {DatabaseInstance} from '@typings/database/database';
import mix from '@utils/mix';
interface Operator extends BaseHandlerMix, PostHandlerMix, UserHandlerMix, GroupHandlerMix, ChannelHandlerMix, TeamHandlerMix {}
class Operator extends mix(BaseHandler).with(PostHandler, UserHandler, GroupHandler, ChannelHandler, TeamHandler) {
database: DatabaseInstance;
constructor(activeDatabase?: DatabaseInstance) {
super();
if (activeDatabase) {
this.activeDatabase = activeDatabase;
}
}
}
export default Operator;

View File

@@ -1,187 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {
prepareAppRecord,
prepareCustomEmojiRecord,
prepareGlobalRecord,
prepareRoleRecord,
prepareServersRecord,
prepareSystemRecord,
prepareTermsOfServiceRecord,
} from '@database/operator/prepareRecords/general';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
jest.mock('@database/manager');
describe('*** Isolated Prepare Records Test ***', () => {
let databaseManagerClient: DatabaseManager;
beforeAll(async () => {
databaseManagerClient = new DatabaseManager();
});
it('=> prepareAppRecord: should return an array of type App', async () => {
expect.assertions(3);
const database = await databaseManagerClient.getDefaultDatabase();
expect(database).toBeTruthy();
const preparedRecords = await prepareAppRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
build_number: 'build-7',
created_at: 1,
version_number: 'v-1',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('App');
});
it('=> prepareGlobalRecord: should return an array of type Global', async () => {
expect.assertions(3);
const database = await databaseManagerClient.getDefaultDatabase();
expect(database).toBeTruthy();
const preparedRecords = await prepareGlobalRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {name: 'g-n1', value: 'g-v1'},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Global');
});
it('=> prepareServersRecord: should return an array of type Servers', async () => {
expect.assertions(3);
const database = await databaseManagerClient.getDefaultDatabase();
expect(database).toBeTruthy();
const preparedRecords = await prepareServersRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
db_path: 'mm-server',
display_name: 's-displayName',
mention_count: 1,
unread_count: 0,
url: 'https://community.mattermost.com',
isSecured: true,
lastActiveAt: 1623926359,
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Servers');
});
it('=> prepareRoleRecord: should return an array of type Role', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'isolated_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareRoleRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'role-1',
name: 'role-name-1',
permissions: [],
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Role');
});
it('=> prepareSystemRecord: should return an array of type System', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'isolated_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareSystemRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {id: 'system-1', name: 'system-name-1', value: 'system'},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('System');
});
it('=> prepareTermsOfServiceRecord: should return an array of type TermsOfService', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'isolated_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareTermsOfServiceRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'tos-1',
accepted_at: 1,
create_at: 1613667352029,
user_id: 'user1613667352029',
text: '',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('TermsOfService');
});
it('=> prepareCustomEmojiRecord: should return an array of type CustomEmoji', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'isolated_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareCustomEmojiRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'i',
create_at: 1580913641769,
update_at: 1580913641769,
delete_at: 0,
creator_id: '4cprpki7ri81mbx8efixcsb8jo',
name: 'boomI',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('CustomEmoji');
});
});

View File

@@ -1,229 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/prepareRecords/index';
import App from '@typings/database/app';
import CustomEmoji from '@typings/database/custom_emoji';
import {
DataFactoryArgs,
RawApp,
RawCustomEmoji,
RawGlobal,
RawRole,
RawServers,
RawSystem,
RawTermsOfService,
} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
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';
const {APP, GLOBAL, SERVERS} = MM_TABLES.DEFAULT;
const {
CUSTOM_EMOJI,
ROLE,
SYSTEM,
TERMS_OF_SERVICE,
} = MM_TABLES.SERVER;
/**
* prepareAppRecord: Prepares record of entity 'App' from the DEFAULT database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareAppRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawApp;
const record = value.record as App;
const isCreateAction = action === OperationType.CREATE;
const generator = (app: App) => {
app._raw.id = isCreateAction ? app.id : record.id;
app.buildNumber = raw?.build_number;
app.createdAt = raw?.created_at;
app.versionNumber = raw?.version_number;
};
return prepareBaseRecord({
action,
database,
generator,
tableName: APP,
value,
});
};
/**
* prepareGlobalRecord: Prepares record of entity 'Global' from the DEFAULT database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareGlobalRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawGlobal;
const record = value.record as Global;
const isCreateAction = action === OperationType.CREATE;
const generator = (global: Global) => {
global._raw.id = isCreateAction ? global.id : record.id;
global.name = raw?.name;
global.value = raw?.value;
};
return prepareBaseRecord({
action,
database,
generator,
tableName: GLOBAL,
value,
});
};
/**
* prepareServersRecord: Prepares record of entity 'Servers' from the DEFAULT database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareServersRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawServers;
const record = value.record as Servers;
const isCreateAction = action === OperationType.CREATE;
const generator = (servers: Servers) => {
servers._raw.id = isCreateAction ? servers.id : record.id;
servers.dbPath = raw?.db_path;
servers.displayName = raw?.display_name;
servers.mentionCount = raw?.mention_count;
servers.unreadCount = raw?.unread_count;
servers.url = raw?.url;
servers.isSecured = raw?.isSecured;
servers.lastActiveAt = raw?.lastActiveAt;
};
return prepareBaseRecord({
action,
database,
tableName: SERVERS,
value,
generator,
});
};
/**
* prepareCustomEmojiRecord: Prepares record of entity 'CustomEmoji' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareCustomEmojiRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawCustomEmoji;
const record = value.record as CustomEmoji;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const generator = (emoji: CustomEmoji) => {
emoji._raw.id = isCreateAction ? (raw?.id ?? emoji.id) : record.id;
emoji.name = raw.name;
};
return prepareBaseRecord({
action,
database,
tableName: CUSTOM_EMOJI,
value,
generator,
});
};
/**
* prepareRoleRecord: Prepares record of entity 'Role' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareRoleRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawRole;
const record = value.record as Role;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const generator = (role: Role) => {
role._raw.id = isCreateAction ? (raw?.id ?? role.id) : record.id;
role.name = raw?.name;
role.permissions = raw?.permissions;
};
return prepareBaseRecord({
action,
database,
tableName: ROLE,
value,
generator,
});
};
/**
* prepareSystemRecord: Prepares record of entity 'System' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareSystemRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawSystem;
const record = value.record as System;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const generator = (system: System) => {
system._raw.id = isCreateAction ? (raw?.id ?? system.id) : record.id;
system.name = raw?.name;
system.value = raw?.value;
};
return prepareBaseRecord({
action,
database,
tableName: SYSTEM,
value,
generator,
});
};
/**
* prepareTermsOfServiceRecord: Prepares record of entity 'TermsOfService' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareTermsOfServiceRecord = ({action, database, value}: DataFactoryArgs) => {
const raw = value.raw as RawTermsOfService;
const record = value.record as TermsOfService;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const generator = (tos: TermsOfService) => {
tos._raw.id = isCreateAction ? (raw?.id ?? tos.id) : record.id;
tos.acceptedAt = raw?.accepted_at;
};
return prepareBaseRecord({
action,
database,
tableName: TERMS_OF_SERVICE,
value,
generator,
});
};

View File

@@ -1,19 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import App from '@typings/database/app';
import Channel from '@typings/database/channel';
import ChannelInfo from '@typings/database/channel_info';
import ChannelMembership from '@typings/database/channel_membership';
import CustomEmoji from '@typings/database/custom_emoji';
import Channel from '@typings/database/models/servers/channel';
import ChannelInfo from '@typings/database/models/servers/channel_info';
import ChannelMembership from '@typings/database/models/servers/channel_membership';
import CustomEmoji from '@typings/database/models/servers/custom_emoji';
import {
RawApp,
RawChannel,
RawChannelInfo,
RawChannelMembership,
RawCustomEmoji,
RawDraft,
RawGlobal,
RawGroup,
RawGroupMembership,
RawGroupsInChannel,
@@ -24,7 +21,6 @@ import {
RawPost,
RawPreference,
RawRole,
RawServers,
RawSlashCommand,
RawSystem,
RawTeam,
@@ -34,27 +30,25 @@ import {
RawTermsOfService,
RawUser,
} from '@typings/database/database';
import Draft from '@typings/database/draft';
import Global from '@typings/database/global';
import Group from '@typings/database/group';
import GroupMembership from '@typings/database/group_membership';
import GroupsInChannel from '@typings/database/groups_in_channel';
import GroupsInTeam from '@typings/database/groups_in_team';
import MyChannel from '@typings/database/my_channel';
import MyChannelSettings from '@typings/database/my_channel_settings';
import MyTeam from '@typings/database/my_team';
import Post from '@typings/database/post';
import Preference from '@typings/database/preference';
import Role from '@typings/database/role';
import Servers from '@typings/database/servers';
import SlashCommand from '@typings/database/slash_command';
import System from '@typings/database/system';
import Team from '@typings/database/team';
import TeamChannelHistory from '@typings/database/team_channel_history';
import TeamMembership from '@typings/database/team_membership';
import TeamSearchHistory from '@typings/database/team_search_history';
import TermsOfService from '@typings/database/terms_of_service';
import User from '@typings/database/user';
import Draft from '@typings/database/models/servers/draft';
import Group from '@typings/database/models/servers/group';
import GroupMembership from '@typings/database/models/servers/group_membership';
import GroupsInChannel from '@typings/database/models/servers/groups_in_channel';
import GroupsInTeam from '@typings/database/models/servers/groups_in_team';
import MyChannel from '@typings/database/models/servers/my_channel';
import MyChannelSettings from '@typings/database/models/servers/my_channel_settings';
import MyTeam from '@typings/database/models/servers/my_team';
import Post from '@typings/database/models/servers/post';
import Preference from '@typings/database/models/servers/preference';
import Role from '@typings/database/models/servers/role';
import SlashCommand from '@typings/database/models/servers/slash_command';
import System from '@typings/database/models/servers/system';
import Team from '@typings/database/models/servers/team';
import TeamChannelHistory from '@typings/database/models/servers/team_channel_history';
import TeamMembership from '@typings/database/models/servers/team_membership';
import TeamSearchHistory from '@typings/database/models/servers/team_search_history';
import TermsOfService from '@typings/database/models/servers/terms_of_service';
import User from '@typings/database/models/servers/user';
/**
* This file contains all the comparators that are used by the handlers to find out which records to truly update and
@@ -63,18 +57,6 @@ import User from '@typings/database/user';
* 'record' and the 'raw'
*/
export const isRecordAppEqualToRaw = (record: App, raw: RawApp) => {
return (raw.build_number === record.buildNumber && raw.version_number === record.versionNumber);
};
export const isRecordGlobalEqualToRaw = (record: Global, raw: RawGlobal) => {
return raw.name === record.name && raw.value === record.value;
};
export const isRecordServerEqualToRaw = (record: Servers, raw: RawServers) => {
return raw.url === record.url && raw.db_path === record.dbPath;
};
export const isRecordRoleEqualToRaw = (record: Role, raw: RawRole) => {
return raw.id === record.id;
};

View File

@@ -0,0 +1,169 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {
isRecordChannelEqualToRaw,
isRecordChannelInfoEqualToRaw,
isRecordMyChannelEqualToRaw,
isRecordMyChannelSettingsEqualToRaw,
} from '@database/operator/server_data_operator/comparators';
import {
transformChannelInfoRecord,
transformChannelRecord,
transformMyChannelRecord,
transformMyChannelSettingsRecord,
} from '@database/operator/server_data_operator/transformers/channel';
import ServerDataOperator from '..';
import type {RawChannel} from '@typings/database/database';
describe('*** Operator: Channel Handlers tests ***', () => {
let operator: ServerDataOperator;
beforeAll(async () => {
await DatabaseManager.init(['baseHandler.test.com']);
operator = DatabaseManager.serverDatabases['baseHandler.test.com'].operator;
});
it('=> HandleChannel: should write to the CHANNEL table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const channels: RawChannel[] = [
{
id: 'kjlw9j1ttnxwig7tnqgebg7dtipno',
create_at: 1600185541285,
update_at: 1604401077256,
delete_at: 0,
team_id: '',
type: 'D',
display_name: '',
name: 'gh781zkzkhh357b4bejephjz5u8daw__9ciscaqbrpd6d8s68k76xb9bte',
header: '(https://mattermost',
purpose: '',
last_post_at: 1617311494451,
total_msg_count: 585,
extra_update_at: 0,
creator_id: '',
group_constrained: null,
shared: false,
props: null,
scheme_id: null,
},
];
await operator.handleChannel({
channels,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'id',
createOrUpdateRawValues: channels,
tableName: 'Channel',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordChannelEqualToRaw,
transformer: transformChannelRecord,
});
});
it('=> HandleMyChannelSettings: should write to the MY_CHANNEL_SETTINGS table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const settings = [
{
channel_id: 'c',
notify_props: {
desktop: 'all',
desktop_sound: true,
email: true,
first_name: true,
mention_keys: '',
push: 'mention',
channel: true,
},
},
];
await operator.handleMyChannelSettings({
settings,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'channel_id',
createOrUpdateRawValues: settings,
tableName: 'MyChannelSettings',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordMyChannelSettingsEqualToRaw,
transformer: transformMyChannelSettingsRecord,
});
});
it('=> HandleChannelInfo: should write to the CHANNEL_INFO table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator as any, 'handleRecords');
const channelInfos = [
{
channel_id: 'c',
guest_count: 10,
header: 'channel info header',
member_count: 10,
pinned_post_count: 3,
purpose: 'sample channel ',
},
];
await operator.handleChannelInfo({
channelInfos,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'channel_id',
createOrUpdateRawValues: channelInfos,
tableName: 'ChannelInfo',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordChannelInfoEqualToRaw,
transformer: transformChannelInfoRecord,
});
});
it('=> HandleMyChannel: should write to the MY_CHANNEL table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const myChannels = [
{
channel_id: 'c',
last_post_at: 1617311494451,
last_viewed_at: 1617311494451,
mentions_count: 3,
message_count: 10,
roles: 'guest',
},
];
await operator.handleMyChannel({
myChannels,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'channel_id',
createOrUpdateRawValues: myChannels,
tableName: 'MyChannel',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordMyChannelEqualToRaw,
transformer: transformMyChannelRecord,
});
});
});

View File

@@ -8,24 +8,24 @@ import {
isRecordChannelInfoEqualToRaw,
isRecordMyChannelEqualToRaw,
isRecordMyChannelSettingsEqualToRaw,
} from '@database/operator/comparators';
} from '@database/operator/server_data_operator/comparators';
import {
prepareChannelInfoRecord,
prepareChannelRecord,
prepareMyChannelRecord,
prepareMyChannelSettingsRecord,
} from '@database/operator/prepareRecords/channel';
transformChannelInfoRecord,
transformChannelRecord,
transformMyChannelRecord,
transformMyChannelSettingsRecord,
} from '@database/operator/server_data_operator/transformers/channel';
import {getUniqueRawsBy} from '@database/operator/utils/general';
import Channel from '@typings/database/channel';
import ChannelInfo from '@typings/database/channel_info';
import Channel from '@typings/database/models/servers/channel';
import ChannelInfo from '@typings/database/models/servers/channel_info';
import {
HandleChannelArgs,
HandleChannelInfoArgs,
HandleMyChannelArgs,
HandleMyChannelSettingsArgs,
} from '@typings/database/database';
import MyChannel from '@typings/database/my_channel';
import MyChannelSettings from '@typings/database/my_channel_settings';
import MyChannel from '@typings/database/models/servers/my_channel';
import MyChannelSettings from '@typings/database/models/servers/my_channel_settings';
const {
CHANNEL,
@@ -43,7 +43,7 @@ export interface ChannelHandlerMix {
const ChannelHandler = (superclass: any) => class extends superclass {
/**
* handleChannel: Handler responsible for the Create/Update operations occurring on the CHANNEL entity from the 'Server' schema
* handleChannel: Handler responsible for the Create/Update operations occurring on the CHANNEL table from the 'Server' schema
* @param {HandleChannelArgs} channelsArgs
* @param {RawChannel[]} channelsArgs.channels
* @param {boolean} channelsArgs.prepareRecordsOnly
@@ -59,14 +59,14 @@ const ChannelHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({raws: channels, key: 'id'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: channels, key: 'id'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'id',
findMatchingRecordBy: isRecordChannelEqualToRaw,
operator: prepareChannelRecord,
transformer: transformChannelRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: CHANNEL,
});
@@ -74,7 +74,7 @@ const ChannelHandler = (superclass: any) => class extends superclass {
};
/**
* handleMyChannelSettings: Handler responsible for the Create/Update operations occurring on the MY_CHANNEL_SETTINGS entity from the 'Server' schema
* handleMyChannelSettings: Handler responsible for the Create/Update operations occurring on the MY_CHANNEL_SETTINGS table from the 'Server' schema
* @param {HandleMyChannelSettingsArgs} settingsArgs
* @param {RawMyChannelSettings[]} settingsArgs.settings
* @param {boolean} settingsArgs.prepareRecordsOnly
@@ -90,14 +90,14 @@ const ChannelHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({raws: settings, key: 'channel_id'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: settings, key: 'channel_id'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'channel_id',
findMatchingRecordBy: isRecordMyChannelSettingsEqualToRaw,
operator: prepareMyChannelSettingsRecord,
transformer: transformMyChannelSettingsRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: MY_CHANNEL_SETTINGS,
});
@@ -105,7 +105,7 @@ const ChannelHandler = (superclass: any) => class extends superclass {
};
/**
* handleChannelInfo: Handler responsible for the Create/Update operations occurring on the CHANNEL_INFO entity from the 'Server' schema
* handleChannelInfo: Handler responsible for the Create/Update operations occurring on the CHANNEL_INFO table from the 'Server' schema
* @param {HandleChannelInfoArgs} channelInfosArgs
* @param {RawChannelInfo[]} channelInfosArgs.channelInfos
* @param {boolean} channelInfosArgs.prepareRecordsOnly
@@ -121,17 +121,17 @@ const ChannelHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({
const createOrUpdateRawValues = getUniqueRawsBy({
raws: channelInfos,
key: 'channel_id',
});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'channel_id',
findMatchingRecordBy: isRecordChannelInfoEqualToRaw,
operator: prepareChannelInfoRecord,
transformer: transformChannelInfoRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: CHANNEL_INFO,
});
@@ -139,7 +139,7 @@ const ChannelHandler = (superclass: any) => class extends superclass {
};
/**
* handleMyChannel: Handler responsible for the Create/Update operations occurring on the MY_CHANNEL entity from the 'Server' schema
* handleMyChannel: Handler responsible for the Create/Update operations occurring on the MY_CHANNEL table from the 'Server' schema
* @param {HandleMyChannelArgs} myChannelsArgs
* @param {RawMyChannel[]} myChannelsArgs.myChannels
* @param {boolean} myChannelsArgs.prepareRecordsOnly
@@ -155,17 +155,17 @@ const ChannelHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({
const createOrUpdateRawValues = getUniqueRawsBy({
raws: myChannels,
key: 'channel_id',
});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'channel_id',
findMatchingRecordBy: isRecordMyChannelEqualToRaw,
operator: prepareMyChannelRecord,
transformer: transformMyChannelRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: MY_CHANNEL,
});

View File

@@ -0,0 +1,159 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {
isRecordGroupEqualToRaw,
isRecordGroupMembershipEqualToRaw,
isRecordGroupsInChannelEqualToRaw,
isRecordGroupsInTeamEqualToRaw,
} from '@database/operator/server_data_operator/comparators';
import {
transformGroupMembershipRecord,
transformGroupRecord,
transformGroupsInChannelRecord,
transformGroupsInTeamRecord,
} from '@database/operator/server_data_operator/transformers/group';
import ServerDataOperator from '..';
describe('*** Operator: Group Handlers tests ***', () => {
let operator: ServerDataOperator;
beforeAll(async () => {
await DatabaseManager.init(['baseHandler.test.com']);
operator = DatabaseManager.serverDatabases['baseHandler.test.com'].operator;
});
it('=> HandleGroup: should write to the GROUP table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const groups = [
{
id: 'id_groupdfjdlfkjdkfdsf',
name: 'mobile_team',
display_name: 'mobile team',
description: '',
source: '',
remote_id: '',
create_at: 0,
update_at: 0,
delete_at: 0,
has_syncables: true,
},
];
await operator.handleGroup({
groups,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'name',
createOrUpdateRawValues: groups,
tableName: 'Group',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupEqualToRaw,
transformer: transformGroupRecord,
});
});
it('=> HandleGroupsInTeam: should write to the GROUPS_IN_TEAM table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const groupsInTeams = [
{
team_id: 'team_899',
team_display_name: '',
team_type: '',
group_id: 'group_id89',
auto_add: true,
create_at: 0,
delete_at: 0,
update_at: 0,
},
];
await operator.handleGroupsInTeam({
groupsInTeams,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'group_id',
createOrUpdateRawValues: groupsInTeams,
tableName: 'GroupsInTeam',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupsInTeamEqualToRaw,
transformer: transformGroupsInTeamRecord,
});
});
it('=> HandleGroupsInChannel: should write to the GROUPS_IN_CHANNEL table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const groupsInChannels = [
{
auto_add: true,
channel_display_name: '',
channel_id: 'channelid',
channel_type: '',
create_at: 0,
delete_at: 0,
group_id: 'groupId',
team_display_name: '',
team_id: '',
team_type: '',
update_at: 0,
member_count: 0,
timezone_count: 0,
},
];
await operator.handleGroupsInChannel({
groupsInChannels,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'group_id',
createOrUpdateRawValues: groupsInChannels,
tableName: 'GroupsInChannel',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupsInChannelEqualToRaw,
transformer: transformGroupsInChannelRecord,
});
});
it('=> HandleGroupMembership: should write to the GROUP_MEMBERSHIP table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const groupMemberships = [
{
user_id: 'u4cprpki7ri81mbx8efixcsb8jo',
group_id: 'g4cprpki7ri81mbx8efixcsb8jo',
},
];
await operator.handleGroupMembership({
groupMemberships,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
createOrUpdateRawValues: groupMemberships,
tableName: 'GroupMembership',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordGroupMembershipEqualToRaw,
transformer: transformGroupMembershipRecord,
});
});
});

View File

@@ -8,13 +8,13 @@ import {
isRecordGroupMembershipEqualToRaw,
isRecordGroupsInChannelEqualToRaw,
isRecordGroupsInTeamEqualToRaw,
} from '@database/operator/comparators';
} from '@database/operator/server_data_operator/comparators';
import {
prepareGroupMembershipRecord,
prepareGroupRecord,
prepareGroupsInChannelRecord,
prepareGroupsInTeamRecord,
} from '@database/operator/prepareRecords/group';
transformGroupMembershipRecord,
transformGroupRecord,
transformGroupsInChannelRecord,
transformGroupsInTeamRecord,
} from '@database/operator/server_data_operator/transformers/group';
import {getUniqueRawsBy} from '@database/operator/utils/general';
import {
HandleGroupArgs,
@@ -22,10 +22,10 @@ import {
HandleGroupsInChannelArgs,
HandleGroupsInTeamArgs,
} from '@typings/database/database';
import Group from '@typings/database/group';
import GroupMembership from '@typings/database/group_membership';
import GroupsInChannel from '@typings/database/groups_in_channel';
import GroupsInTeam from '@typings/database/groups_in_team';
import Group from '@typings/database/models/servers/group';
import GroupMembership from '@typings/database/models/servers/group_membership';
import GroupsInChannel from '@typings/database/models/servers/groups_in_channel';
import GroupsInTeam from '@typings/database/models/servers/groups_in_team';
const {
GROUP,
@@ -43,7 +43,7 @@ export interface GroupHandlerMix {
const GroupHandler = (superclass: any) => class extends superclass {
/**
* handleGroupMembership: Handler responsible for the Create/Update operations occurring on the GROUP_MEMBERSHIP entity from the 'Server' schema
* handleGroupMembership: Handler responsible for the Create/Update operations occurring on the GROUP_MEMBERSHIP table from the 'Server' schema
* @param {HandleGroupMembershipArgs} groupMembershipsArgs
* @param {RawGroupMembership[]} groupMembershipsArgs.groupMemberships
* @param {boolean} groupMembershipsArgs.prepareRecordsOnly
@@ -59,14 +59,14 @@ const GroupHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({raws: groupMemberships, key: 'group_id'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: groupMemberships, key: 'group_id'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'user_id',
findMatchingRecordBy: isRecordGroupMembershipEqualToRaw,
operator: prepareGroupMembershipRecord,
transformer: transformGroupMembershipRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: GROUP_MEMBERSHIP,
});
@@ -74,7 +74,7 @@ const GroupHandler = (superclass: any) => class extends superclass {
};
/**
* handleGroup: Handler responsible for the Create/Update operations occurring on the GROUP entity from the 'Server' schema
* handleGroup: Handler responsible for the Create/Update operations occurring on the GROUP table from the 'Server' schema
* @param {HandleGroupArgs} groupsArgs
* @param {RawGroup[]} groupsArgs.groups
* @param {boolean} groupsArgs.prepareRecordsOnly
@@ -90,14 +90,14 @@ const GroupHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({raws: groups, key: 'name'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: groups, key: 'name'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'name',
findMatchingRecordBy: isRecordGroupEqualToRaw,
operator: prepareGroupRecord,
transformer: transformGroupRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: GROUP,
});
@@ -105,7 +105,7 @@ const GroupHandler = (superclass: any) => class extends superclass {
};
/**
* handleGroupsInTeam: Handler responsible for the Create/Update operations occurring on the GROUPS_IN_TEAM entity from the 'Server' schema
* handleGroupsInTeam: Handler responsible for the Create/Update operations occurring on the GROUPS_IN_TEAM table from the 'Server' schema
* @param {HandleGroupsInTeamArgs} groupsInTeamsArgs
* @param {RawGroupsInTeam[]} groupsInTeamsArgs.groupsInTeams
* @param {boolean} groupsInTeamsArgs.prepareRecordsOnly
@@ -121,14 +121,14 @@ const GroupHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({raws: groupsInTeams, key: 'group_id'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: groupsInTeams, key: 'group_id'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'group_id',
findMatchingRecordBy: isRecordGroupsInTeamEqualToRaw,
operator: prepareGroupsInTeamRecord,
transformer: transformGroupsInTeamRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: GROUPS_IN_TEAM,
});
@@ -136,7 +136,7 @@ const GroupHandler = (superclass: any) => class extends superclass {
};
/**
* handleGroupsInChannel: Handler responsible for the Create/Update operations occurring on the GROUPS_IN_CHANNEL entity from the 'Server' schema
* handleGroupsInChannel: Handler responsible for the Create/Update operations occurring on the GROUPS_IN_CHANNEL table from the 'Server' schema
* @param {HandleGroupsInChannelArgs} groupsInChannelsArgs
* @param {RawGroupsInChannel[]} groupsInChannelsArgs.groupsInChannels
* @param {boolean} groupsInChannelsArgs.prepareRecordsOnly
@@ -152,14 +152,14 @@ const GroupHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({raws: groupsInChannels, key: 'channel_id'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: groupsInChannels, key: 'channel_id'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'group_id',
findMatchingRecordBy: isRecordGroupsInChannelEqualToRaw,
operator: prepareGroupsInChannelRecord,
transformer: transformGroupsInChannelRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: GROUPS_IN_CHANNEL,
});

View File

@@ -0,0 +1,158 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DataOperatorException from '@database/exceptions/data_operator_exception';
import DatabaseManager from '@database/manager';
import {
isRecordCustomEmojiEqualToRaw,
isRecordRoleEqualToRaw,
isRecordSystemEqualToRaw,
isRecordTermsOfServiceEqualToRaw,
} from '@database/operator/server_data_operator/comparators';
import {
transformCustomEmojiRecord,
transformRoleRecord,
transformSystemRecord,
transformTermsOfServiceRecord,
} from '@database/operator/server_data_operator/transformers/general';
import {RawRole, RawTermsOfService} from '@typings/database/database';
import ServerDataOperator from '..';
describe('*** DataOperator: Base Handlers tests ***', () => {
let operator: ServerDataOperator;
beforeAll(async () => {
await DatabaseManager.init(['baseHandler.test.com']);
operator = DatabaseManager.serverDatabases['baseHandler.test.com'].operator;
});
it('=> HandleRole: should write to the ROLE table', async () => {
expect.assertions(1);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const roles: RawRole[] = [
{
id: 'custom-role-id-1',
name: 'custom-role-1',
permissions: ['custom-permission-1'],
},
];
await operator.handleRole({
roles,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'id',
transformer: transformRoleRecord,
findMatchingRecordBy: isRecordRoleEqualToRaw,
createOrUpdateRawValues: roles,
tableName: 'Role',
prepareRecordsOnly: false,
});
});
it('=> HandleCustomEmojis: should write to the CUSTOM_EMOJI table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const emojis = [
{
id: 'i',
create_at: 1580913641769,
update_at: 1580913641769,
delete_at: 0,
creator_id: '4cprpki7ri81mbx8efixcsb8jo',
name: 'boomI',
},
];
await operator.handleCustomEmojis({
emojis,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'id',
createOrUpdateRawValues: emojis,
tableName: 'CustomEmoji',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordCustomEmojiEqualToRaw,
transformer: transformCustomEmojiRecord,
});
});
it('=> HandleSystem: should write to the SYSTEM table', async () => {
expect.assertions(1);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const systems = [{name: 'system-1', value: 'system-1'}];
await operator.handleSystem({
systems,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledWith({
findMatchingRecordBy: isRecordSystemEqualToRaw,
fieldName: 'name',
transformer: transformSystemRecord,
createOrUpdateRawValues: systems,
tableName: 'System',
prepareRecordsOnly: false,
});
});
it('=> HandleTermsOfService: should write to the TERMS_OF_SERVICE table', async () => {
expect.assertions(1);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const termOfService: RawTermsOfService[] = [
{
id: 'tos-1',
accepted_at: 1,
create_at: 1613667352029,
user_id: 'user1613667352029',
text: '',
},
];
await operator.handleTermOfService({
termOfService,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledWith({
findMatchingRecordBy: isRecordTermsOfServiceEqualToRaw,
fieldName: 'id',
transformer: transformTermsOfServiceRecord,
createOrUpdateRawValues: termOfService,
tableName: 'TermsOfService',
prepareRecordsOnly: false,
});
});
it('=> No table name: should not call execute if tableName is invalid', async () => {
expect.assertions(3);
const appDatabase = DatabaseManager.appDatabase?.database;
const appOperator = DatabaseManager.appDatabase?.operator;
expect(appDatabase).toBeTruthy();
expect(appOperator).toBeTruthy();
await expect(
operator?.handleRecords({
fieldName: 'invalidField',
tableName: 'INVALID_TABLE_NAME',
// @ts-expect-error: Type does not match RawValue
createOrUpdateRawValues: [{id: 'tos-1', accepted_at: 1}],
}),
).rejects.toThrow(DataOperatorException);
});
});

View File

@@ -0,0 +1,122 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import BaseDataOperator from '@database/operator/base_data_operator';
import DataOperatorException from '@database/exceptions/data_operator_exception';
import {
isRecordCustomEmojiEqualToRaw,
isRecordRoleEqualToRaw,
isRecordSystemEqualToRaw,
isRecordTermsOfServiceEqualToRaw,
} from '@database/operator/server_data_operator/comparators';
import {
transformCustomEmojiRecord,
transformRoleRecord,
transformSystemRecord,
transformTermsOfServiceRecord,
} from '@database/operator/server_data_operator/transformers/general';
import {getUniqueRawsBy} from '@database/operator/utils/general';
import {HandleCustomEmojiArgs, HandleRoleArgs, HandleSystemArgs, HandleTOSArgs, OperationArgs} from '@typings/database/database';
const {SERVER: {CUSTOM_EMOJI, ROLE, SYSTEM, TERMS_OF_SERVICE}} = MM_TABLES;
export default class ServerDataOperatorBase extends BaseDataOperator {
handleRole = async ({roles, prepareRecordsOnly = true}: HandleRoleArgs) => {
if (!roles.length) {
throw new DataOperatorException(
'An empty "values" array has been passed to the handleRole',
);
}
const records = await this.handleRecords({
fieldName: 'id',
findMatchingRecordBy: isRecordRoleEqualToRaw,
transformer: transformRoleRecord,
prepareRecordsOnly,
createOrUpdateRawValues: getUniqueRawsBy({raws: roles, key: 'id'}),
tableName: ROLE,
});
return records;
}
handleCustomEmojis = async ({emojis, prepareRecordsOnly = true}: HandleCustomEmojiArgs) => {
if (!emojis.length) {
throw new DataOperatorException(
'An empty "values" array has been passed to the handleCustomEmojis',
);
}
const records = await this.handleRecords({
fieldName: 'id',
findMatchingRecordBy: isRecordCustomEmojiEqualToRaw,
transformer: transformCustomEmojiRecord,
prepareRecordsOnly,
createOrUpdateRawValues: getUniqueRawsBy({raws: emojis, key: 'id'}),
tableName: CUSTOM_EMOJI,
});
return records;
}
handleSystem = async ({systems, prepareRecordsOnly = true}: HandleSystemArgs) => {
if (!systems.length) {
throw new DataOperatorException(
'An empty "values" array has been passed to the handleSystem',
);
}
const records = await this.handleRecords({
fieldName: 'name',
findMatchingRecordBy: isRecordSystemEqualToRaw,
transformer: transformSystemRecord,
prepareRecordsOnly,
createOrUpdateRawValues: getUniqueRawsBy({raws: systems, key: 'name'}),
tableName: SYSTEM,
});
return records;
}
handleTermOfService = async ({termOfService, prepareRecordsOnly = true}: HandleTOSArgs) => {
if (!termOfService.length) {
throw new DataOperatorException(
'An empty "values" array has been passed to the handleTermOfService',
);
}
const records = await this.handleRecords({
fieldName: 'id',
findMatchingRecordBy: isRecordTermsOfServiceEqualToRaw,
transformer: transformTermsOfServiceRecord,
prepareRecordsOnly,
createOrUpdateRawValues: getUniqueRawsBy({raws: termOfService, key: 'id'}),
tableName: TERMS_OF_SERVICE,
});
return records;
}
/**
* execute: Handles the Create/Update operations on an table.
* @param {OperationArgs} execute
* @param {string} execute.tableName
* @param {RecordValue[]} execute.createRaws
* @param {RecordValue[]} execute.updateRaws
* @param {(TransformerArgs) => Promise<Model>} execute.recordOperator
* @returns {Promise<void>}
*/
execute = async ({createRaws, transformer, tableName, updateRaws}: OperationArgs) => {
const models = await this.prepareRecords({
tableName,
createRaws,
updateRaws,
transformer,
});
if (models?.length > 0) {
await this.batchRecords(models);
}
};
}

View File

@@ -2,42 +2,24 @@
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import Operator from '@database/operator';
import {isRecordDraftEqualToRaw} from '@database/operator/comparators';
import {prepareDraftRecord} from '@database/operator/prepareRecords/post';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {DatabaseType} from '@typings/database/enums';
import {isRecordDraftEqualToRaw} from '@database/operator/server_data_operator/comparators';
import {transformDraftRecord} from '@database/operator/server_data_operator/transformers/post';
jest.mock('@database/manager');
/* eslint-disable @typescript-eslint/no-explicit-any */
import ServerDataOperator from '..';
describe('*** Operator: Post Handlers tests ***', () => {
let databaseManagerClient: DatabaseManager;
let operatorClient: Operator;
let operator: ServerDataOperator;
beforeAll(async () => {
databaseManagerClient = new DatabaseManager();
const database = await databaseManagerClient.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
dbName: 'base_handler',
dbType: DatabaseType.SERVER,
serverUrl: 'baseHandler.test.com',
},
});
operatorClient = new Operator(database!);
await DatabaseManager.init(['baseHandler.test.com']);
operator = DatabaseManager.serverDatabases['baseHandler.test.com'].operator;
});
it('=> HandleDraft: should write to the Draft entity', async () => {
it('=> HandleDraft: should write to the the Draft table', async () => {
expect.assertions(1);
await createTestConnection({databaseName: 'post_handler', setActive: true});
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
const values = [
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const drafts = [
{
channel_id: '4r9jmr7eqt8dxq3f9woypzurrychannelid',
files: [
@@ -63,19 +45,19 @@ describe('*** Operator: Post Handlers tests ***', () => {
},
];
await operatorClient.handleDraft({drafts: values, prepareRecordsOnly: false});
await operator.handleDraft({drafts, prepareRecordsOnly: false});
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
expect(spyOnHandleRecords).toHaveBeenCalledWith({
findMatchingRecordBy: isRecordDraftEqualToRaw,
fieldName: 'channel_id',
operator: prepareDraftRecord,
rawValues: values,
transformer: transformDraftRecord,
createOrUpdateRawValues: drafts,
tableName: 'Draft',
prepareRecordsOnly: false,
});
});
it('=> HandlePosts: should write to Post and its sub-child entities', async () => {
it('=> HandlePosts: should write to the Post and its sub-child tables', async () => {
expect.assertions(12);
const posts = [
@@ -227,17 +209,15 @@ describe('*** Operator: Post Handlers tests ***', () => {
},
];
const spyOnHandleFiles = jest.spyOn(operatorClient as any, 'handleFiles');
const spyOnHandlePostMetadata = jest.spyOn(operatorClient as any, 'handlePostMetadata');
const spyOnHandleReactions = jest.spyOn(operatorClient as any, 'handleReactions');
const spyOnHandleCustomEmojis = jest.spyOn(operatorClient as any, 'handleIsolatedEntity');
const spyOnHandlePostsInThread = jest.spyOn(operatorClient as any, 'handlePostsInThread');
const spyOnHandlePostsInChannel = jest.spyOn(operatorClient as any, 'handlePostsInChannel');
await createTestConnection({databaseName: 'post_handler', setActive: true});
const spyOnHandleFiles = jest.spyOn(operator, 'handleFiles');
const spyOnHandlePostMetadata = jest.spyOn(operator, 'handlePostMetadata');
const spyOnHandleReactions = jest.spyOn(operator, 'handleReactions');
const spyOnHandleCustomEmojis = jest.spyOn(operator, 'handleCustomEmojis');
const spyOnHandlePostsInThread = jest.spyOn(operator, 'handlePostsInThread');
const spyOnHandlePostsInChannel = jest.spyOn(operator, 'handlePostsInChannel');
// handlePosts will in turn call handlePostsInThread
await operatorClient.handlePosts({
await operator.handlePosts({
orders: [
'8swgtrrdiff89jnsiwiip3y1eoe',
'8fcnk3p1jt8mmkaprgajoxz115a',
@@ -338,9 +318,8 @@ describe('*** Operator: Post Handlers tests ***', () => {
expect(spyOnHandleCustomEmojis).toHaveBeenCalledTimes(1);
expect(spyOnHandleCustomEmojis).toHaveBeenCalledWith({
tableName: 'CustomEmoji',
prepareRecordsOnly: false,
values: [
emojis: [
{
id: 'dgwyadacdbbwjc8t357h6hwsrh',
create_at: 1502389307432,

View File

@@ -6,15 +6,15 @@ import Model from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import DataOperatorException from '@database/exceptions/data_operator_exception';
import {isRecordDraftEqualToRaw, isRecordPostEqualToRaw} from '@database/operator/comparators';
import {isRecordDraftEqualToRaw, isRecordPostEqualToRaw} from '@database/operator/server_data_operator/comparators';
import {
prepareDraftRecord,
prepareFileRecord,
preparePostInThreadRecord,
preparePostMetadataRecord,
preparePostRecord,
preparePostsInChannelRecord,
} from '@database/operator/prepareRecords/post';
transformDraftRecord,
transformFileRecord,
transformPostInThreadRecord,
transformPostMetadataRecord,
transformPostRecord,
transformPostsInChannelRecord,
} from '@database/operator/server_data_operator/transformers/post';
import {getRawRecordPairs, getUniqueRawsBy, retrieveRecords} from '@database/operator/utils/general';
import {createPostsChain, sanitizePosts} from '@database/operator/utils/post';
import {
@@ -31,14 +31,13 @@ import {
RawPostsInThread,
RawReaction, RecordPair,
} from '@typings/database/database';
import Draft from '@typings/database/draft';
import {IsolatedEntities} from '@typings/database/enums';
import File from '@typings/database/file';
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 Draft from '@typings/database/models/servers/draft';
import File from '@typings/database/models/servers/file';
import Post from '@typings/database/models/servers/post';
import PostMetadata from '@typings/database/models/servers/post_metadata';
import PostsInChannel from '@typings/database/models/servers/posts_in_channel';
import PostsInThread from '@typings/database/models/servers/posts_in_thread';
import Reaction from '@typings/database/models/servers/reaction';
const {
DRAFT,
@@ -60,7 +59,7 @@ export interface PostHandlerMix {
const PostHandler = (superclass: any) => class extends superclass {
/**
* handleDraft: Handler responsible for the Create/Update operations occurring the Draft entity from the 'Server' schema
* handleDraft: Handler responsible for the Create/Update operations occurring the Draft table from the 'Server' schema
* @param {HandleDraftArgs} draftsArgs
* @param {RawDraft[]} draftsArgs.drafts
* @param {boolean} draftsArgs.prepareRecordsOnly
@@ -72,18 +71,18 @@ const PostHandler = (superclass: any) => class extends superclass {
if (!drafts.length) {
throw new DataOperatorException(
'An empty "drafts" array has been passed to the handleReactions method',
'An empty "drafts" array has been passed to the handleDraft method',
);
}
const rawValues = getUniqueRawsBy({raws: drafts, key: 'channel_id'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: drafts, key: 'channel_id'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'channel_id',
findMatchingRecordBy: isRecordDraftEqualToRaw,
operator: prepareDraftRecord,
transformer: transformDraftRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: DRAFT,
});
@@ -91,7 +90,7 @@ const PostHandler = (superclass: any) => class extends superclass {
};
/**
* handlePosts: Handler responsible for the Create/Update operations occurring on the Post entity from the 'Server' schema
* handlePosts: Handler responsible for the Create/Update operations occurring on the Post table from the 'Server' schema
* @param {HandlePostsArgs} handlePosts
* @param {string[]} handlePosts.orders
* @param {RawPost[]} handlePosts.values
@@ -120,8 +119,8 @@ const PostHandler = (superclass: any) => class extends superclass {
});
// Here we verify in our database that the postsOrdered truly need 'CREATION'
const futureEntries = await this.processInputs({
rawValues: postsOrdered,
const futureEntries = await this.processRecords({
createOrUpdateRawValues: postsOrdered,
tableName,
findMatchingRecordBy: isRecordPostEqualToRaw,
fieldName: 'id',
@@ -143,20 +142,17 @@ const PostHandler = (superclass: any) => class extends superclass {
rawPosts: postsOrdered,
});
const database = await this.getDatabase(tableName);
// Prepares records for batch processing onto the 'Post' entity for the server schema
// Prepares records for batch processing onto the 'Post' table for the server schema
const posts = (await this.prepareRecords({
createRaws: linkedRawPosts,
database,
recordOperator: preparePostRecord,
transformer: transformPostRecord,
tableName,
})) 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
// Starts extracting information from each post to build up for related tables' data
for (const post of postsOrdered) {
// PostInThread handler: checks for id === root_id , if so, then call PostsInThread operator
if (!post.root_id) {
@@ -213,14 +209,13 @@ const PostHandler = (superclass: any) => class extends superclass {
}
if (batch.length) {
await this.batchOperations({database, models: batch});
await this.batchRecords(batch);
}
// LAST: calls handler for CustomEmojis, PostsInThread, PostsInChannel
if (emojis.length) {
await this.handleIsolatedEntity({
tableName: IsolatedEntities.CUSTOM_EMOJI,
values: emojis,
await this.handleCustomEmojis({
emojis,
prepareRecordsOnly: false,
});
}
@@ -236,11 +231,11 @@ const PostHandler = (superclass: any) => class extends superclass {
if (postsUnordered.length) {
// Truly update those posts that have a different update_at value
await this.handleEntityRecords({
await this.handleRecords({
findMatchingRecordBy: isRecordPostEqualToRaw,
fieldName: 'id',
operator: preparePostRecord,
rawValues: postsUnordered,
trasformer: transformPostRecord,
createOrUpdateRawValues: postsUnordered,
tableName: POST,
prepareRecordsOnly: false,
});
@@ -248,7 +243,7 @@ const PostHandler = (superclass: any) => class extends superclass {
};
/**
* handleFiles: Handler responsible for the Create/Update operations occurring on the File entity from the 'Server' schema
* handleFiles: Handler responsible for the Create/Update operations occurring on the File table from the 'Server' schema
* @param {HandleFilesArgs} handleFiles
* @param {RawFile[]} handleFiles.files
* @param {boolean} handleFiles.prepareRecordsOnly
@@ -259,12 +254,9 @@ const PostHandler = (superclass: any) => class extends superclass {
return [];
}
const database = await this.getDatabase(FILE);
const postFiles = await this.prepareRecords({
createRaws: getRawRecordPairs(files),
database,
recordOperator: prepareFileRecord,
transformer: transformFileRecord,
tableName: FILE,
});
@@ -273,14 +265,14 @@ const PostHandler = (superclass: any) => class extends superclass {
}
if (postFiles?.length) {
await this.batchOperations({database, models: [...postFiles]});
await this.batchRecords(postFiles);
}
return [];
};
/**
* handlePostMetadata: Handler responsible for the Create/Update operations occurring on the PostMetadata entity from the 'Server' schema
* handlePostMetadata: Handler responsible for the Create/Update operations occurring on the PostMetadata table from the 'Server' schema
* @param {HandlePostMetadataArgs} handlePostMetadata
* @param {{embed: RawEmbed[], postId: string}[] | undefined} handlePostMetadata.embeds
* @param {{images: Dictionary<PostImage>, postId: string}[] | undefined} handlePostMetadata.images
@@ -317,12 +309,9 @@ const PostHandler = (superclass: any) => class extends superclass {
return [];
}
const database = await this.getDatabase(POST_METADATA);
const postMetas = await this.prepareRecords({
createRaws: getRawRecordPairs(metadata),
database,
recordOperator: preparePostMetadataRecord,
transformer: transformPostMetadataRecord,
tableName: POST_METADATA,
});
@@ -331,14 +320,14 @@ const PostHandler = (superclass: any) => class extends superclass {
}
if (postMetas?.length) {
await this.batchOperations({database, models: [...postMetas]});
await this.batchRecords(postMetas);
}
return [];
};
/**
* handlePostsInThread: Handler responsible for the Create/Update operations occurring on the PostsInThread entity from the 'Server' schema
* handlePostsInThread: Handler responsible for the Create/Update operations occurring on the PostsInThread table from the 'Server' schema
* @param {RawPostsInThread[]} rootPosts
* @returns {Promise<void>}
*/
@@ -350,10 +339,8 @@ const PostHandler = (superclass: any) => class extends superclass {
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.
const threads = (await this.database.collections.
get(POST).
query(Q.where('root_id', Q.oneOf(postIds))).
fetch()) as Post[];
@@ -371,19 +358,18 @@ const PostHandler = (superclass: any) => class extends superclass {
if (rawPostsInThreads.length) {
const postInThreadRecords = (await this.prepareRecords({
createRaws: getRawRecordPairs(rawPostsInThreads),
database,
recordOperator: preparePostInThreadRecord,
transformer: transformPostInThreadRecord,
tableName: POSTS_IN_THREAD,
})) as PostsInThread[];
if (postInThreadRecords?.length) {
await this.batchOperations({database, models: postInThreadRecords});
await this.batchRecords(postInThreadRecords);
}
}
};
/**
* handlePostsInChannel: Handler responsible for the Create/Update operations occurring on the PostsInChannel entity from the 'Server' schema
* handlePostsInChannel: Handler responsible for the Create/Update operations occurring on the PostsInChannel table from the 'Server' schema
* @param {RawPost[]} posts
* @returns {Promise<void>}
*/
@@ -412,21 +398,19 @@ const PostHandler = (superclass: any) => class extends superclass {
// Find highest 'create_at' value in chain; -1 means we are dealing with one item in the posts array
const latest = sortedPosts[sortedPosts.length - 1].create_at;
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 chunks = (await retrieveRecords({
database,
database: this.database,
tableName: POSTS_IN_CHANNEL,
condition: Q.where('channel_id', channelId),
})) as PostsInChannel[];
const createPostsInChannelRecord = async () => {
await this.executeInDatabase({
await this.execute({
createRaws: [{record: undefined, raw: {channel_id: channelId, earliest, latest}}],
tableName: POSTS_IN_CHANNEL,
recordOperator: preparePostsInChannelRecord,
transformer: transformPostsInChannelRecord,
});
};
@@ -459,7 +443,7 @@ const PostHandler = (superclass: any) => class extends superclass {
if (found) {
// We have a potential chunk to plug nearby
const potentialPosts = (await retrieveRecords({
database,
database: this.database,
tableName: POST,
condition: Q.where('create_at', earliest),
})) as Post[];
@@ -472,7 +456,7 @@ const PostHandler = (superclass: any) => class extends superclass {
if (isChainable) {
// Update this chunk's data in PostsInChannel table. earliest comes from tipOfChain while latest comes from chunk
await database.action(async () => {
await this.database.action(async () => {
await targetChunk.update((postInChannel) => {
postInChannel.earliest = earliest;
});

View File

@@ -0,0 +1,231 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {
isRecordMyTeamEqualToRaw,
isRecordSlashCommandEqualToRaw,
isRecordTeamChannelHistoryEqualToRaw,
isRecordTeamEqualToRaw,
isRecordTeamMembershipEqualToRaw,
isRecordTeamSearchHistoryEqualToRaw,
} from '@database/operator/server_data_operator/comparators';
import {
transformMyTeamRecord,
transformSlashCommandRecord,
transformTeamChannelHistoryRecord,
transformTeamMembershipRecord,
transformTeamRecord,
transformTeamSearchHistoryRecord,
} from '@database/operator/server_data_operator/transformers/team';
import ServerDataOperator from '..';
describe('*** Operator: Team Handlers tests ***', () => {
let operator: ServerDataOperator;
beforeAll(async () => {
await DatabaseManager.init(['baseHandler.test.com']);
operator = DatabaseManager.serverDatabases['baseHandler.test.com'].operator;
});
it('=> HandleTeam: should write to the TEAM table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const teams = [
{
id: 'rcgiyftm7jyrxnmdfdfa1osd8zswby',
create_at: 1445538153952,
update_at: 1588876392150,
delete_at: 0,
display_name: 'Contributors',
name: 'core',
description: '',
email: '',
type: 'O',
company_name: '',
allowed_domains: '',
invite_id: 'codoy5s743rq5mk18i7u5dfdfksz7e',
allow_open_invite: true,
last_team_icon_update: 1525181587639,
scheme_id: 'hbwgrncq1pfcdkpotzidfdmarn95o',
group_constrained: null,
},
];
await operator.handleTeam({
teams,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'id',
createOrUpdateRawValues: teams,
tableName: 'Team',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamEqualToRaw,
transformer: transformTeamRecord,
});
});
it('=> HandleTeamMemberships: should write to the TEAM_MEMBERSHIP table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const teamMemberships = [
{
team_id: 'a',
user_id: 'ab',
roles: '3ngdqe1e7tfcbmam4qgnxp91bw',
delete_at: 0,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
];
await operator.handleTeamMemberships({
teamMemberships,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
createOrUpdateRawValues: teamMemberships,
tableName: 'TeamMembership',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamMembershipEqualToRaw,
transformer: transformTeamMembershipRecord,
});
});
it('=> HandleMyTeam: should write to the MY_TEAM table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const myTeams = [
{
team_id: 'teamA',
roles: 'roleA, roleB, roleC',
is_unread: true,
mentions_count: 3,
},
];
await operator.handleMyTeam({
myTeams,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'team_id',
createOrUpdateRawValues: myTeams,
tableName: 'MyTeam',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordMyTeamEqualToRaw,
transformer: transformMyTeamRecord,
});
});
it('=> HandleTeamChannelHistory: should write to the TEAM_CHANNEL_HISTORY table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const teamChannelHistories = [
{
team_id: 'a',
channel_ids: ['ca', 'cb'],
},
];
await operator.handleTeamChannelHistory({
teamChannelHistories,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'team_id',
createOrUpdateRawValues: teamChannelHistories,
tableName: 'TeamChannelHistory',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamChannelHistoryEqualToRaw,
transformer: transformTeamChannelHistoryRecord,
});
});
it('=> HandleTeamSearchHistory: should write to the TEAM_SEARCH_HISTORY table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const teamSearchHistories = [
{
team_id: 'a',
term: 'termA',
display_term: 'termA',
created_at: 1445538153952,
},
];
await operator.handleTeamSearchHistory({
teamSearchHistories,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'team_id',
createOrUpdateRawValues: teamSearchHistories,
tableName: 'TeamSearchHistory',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordTeamSearchHistoryEqualToRaw,
transformer: transformTeamSearchHistoryRecord,
});
});
it('=> HandleSlashCommand: should write to the SLASH_COMMAND table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const slashCommands = [
{
id: 'command_1',
auto_complete: true,
auto_complete_desc: 'mock_command',
auto_complete_hint: 'hint',
create_at: 1445538153952,
creator_id: 'creator_id',
delete_at: 1445538153952,
description: 'description',
display_name: 'display_name',
icon_url: 'display_name',
method: 'get',
team_id: 'teamA',
token: 'token',
trigger: 'trigger',
update_at: 1445538153953,
url: 'url',
username: 'userA',
},
];
await operator.handleSlashCommand({
slashCommands,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'id',
createOrUpdateRawValues: slashCommands,
tableName: 'SlashCommand',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordSlashCommandEqualToRaw,
transformer: transformSlashCommandRecord,
});
});
});

View File

@@ -10,15 +10,15 @@ import {
isRecordTeamEqualToRaw,
isRecordTeamMembershipEqualToRaw,
isRecordTeamSearchHistoryEqualToRaw,
} from '@database/operator/comparators';
} from '@database/operator/server_data_operator/comparators';
import {
prepareMyTeamRecord,
prepareSlashCommandRecord,
prepareTeamChannelHistoryRecord,
prepareTeamMembershipRecord,
prepareTeamRecord,
prepareTeamSearchHistoryRecord,
} from '@database/operator/prepareRecords/team';
transformMyTeamRecord,
transformSlashCommandRecord,
transformTeamChannelHistoryRecord,
transformTeamMembershipRecord,
transformTeamRecord,
transformTeamSearchHistoryRecord,
} from '@database/operator/server_data_operator/transformers/team';
import {getUniqueRawsBy} from '@database/operator/utils/general';
import {
HandleMyTeamArgs,
@@ -28,12 +28,12 @@ import {
HandleTeamMembershipArgs,
HandleTeamSearchHistoryArgs,
} from '@typings/database/database';
import MyTeam from '@typings/database/my_team';
import SlashCommand from '@typings/database/slash_command';
import Team from '@typings/database/team';
import TeamChannelHistory from '@typings/database/team_channel_history';
import TeamMembership from '@typings/database/team_membership';
import TeamSearchHistory from '@typings/database/team_search_history';
import MyTeam from '@typings/database/models/servers/my_team';
import SlashCommand from '@typings/database/models/servers/slash_command';
import Team from '@typings/database/models/servers/team';
import TeamChannelHistory from '@typings/database/models/servers/team_channel_history';
import TeamMembership from '@typings/database/models/servers/team_membership';
import TeamSearchHistory from '@typings/database/models/servers/team_search_history';
const {
MY_TEAM,
@@ -54,7 +54,7 @@ export interface TeamHandlerMix {
const TeamHandler = (superclass: any) => class extends superclass {
/**
* handleTeamMemberships: Handler responsible for the Create/Update operations occurring on the TEAM_MEMBERSHIP entity from the 'Server' schema
* handleTeamMemberships: Handler responsible for the Create/Update operations occurring on the TEAM_MEMBERSHIP table from the 'Server' schema
* @param {HandleTeamMembershipArgs} teamMembershipsArgs
* @param {RawTeamMembership[]} teamMembershipsArgs.teamMemberships
* @param {boolean} teamMembershipsArgs.prepareRecordsOnly
@@ -70,13 +70,13 @@ const TeamHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({raws: teamMemberships, key: 'team_id'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: teamMemberships, key: 'team_id'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'user_id',
findMatchingRecordBy: isRecordTeamMembershipEqualToRaw,
operator: prepareTeamMembershipRecord,
rawValues,
transformer: transformTeamMembershipRecord,
createOrUpdateRawValues,
tableName: TEAM_MEMBERSHIP,
prepareRecordsOnly,
});
@@ -85,7 +85,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
};
/**
* handleTeam: Handler responsible for the Create/Update operations occurring on the TEAM entity from the 'Server' schema
* handleTeam: Handler responsible for the Create/Update operations occurring on the TEAM table from the 'Server' schema
* @param {HandleTeamArgs} teamsArgs
* @param {RawTeam[]} teamsArgs.teams
* @param {boolean} teamsArgs.prepareRecordsOnly
@@ -101,14 +101,14 @@ const TeamHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({raws: teams, key: 'id'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: teams, key: 'id'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'id',
findMatchingRecordBy: isRecordTeamEqualToRaw,
operator: prepareTeamRecord,
transformer: transformTeamRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: TEAM,
});
@@ -116,7 +116,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
};
/**
* handleTeamChannelHistory: Handler responsible for the Create/Update operations occurring on the TEAM_CHANNEL_HISTORY entity from the 'Server' schema
* handleTeamChannelHistory: Handler responsible for the Create/Update operations occurring on the TEAM_CHANNEL_HISTORY table from the 'Server' schema
* @param {HandleTeamChannelHistoryArgs} teamChannelHistoriesArgs
* @param {RawTeamChannelHistory[]} teamChannelHistoriesArgs.teamChannelHistories
* @param {boolean} teamChannelHistoriesArgs.prepareRecordsOnly
@@ -132,14 +132,14 @@ const TeamHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({raws: teamChannelHistories, key: 'team_id'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: teamChannelHistories, key: 'team_id'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'team_id',
findMatchingRecordBy: isRecordTeamChannelHistoryEqualToRaw,
operator: prepareTeamChannelHistoryRecord,
transformer: transformTeamChannelHistoryRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: TEAM_CHANNEL_HISTORY,
});
@@ -147,7 +147,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
};
/**
* handleTeamSearchHistory: Handler responsible for the Create/Update operations occurring on the TEAM_SEARCH_HISTORY entity from the 'Server' schema
* handleTeamSearchHistory: Handler responsible for the Create/Update operations occurring on the TEAM_SEARCH_HISTORY table from the 'Server' schema
* @param {HandleTeamSearchHistoryArgs} teamSearchHistoriesArgs
* @param {RawTeamSearchHistory[]} teamSearchHistoriesArgs.teamSearchHistories
* @param {boolean} teamSearchHistoriesArgs.prepareRecordsOnly
@@ -163,14 +163,14 @@ const TeamHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({raws: teamSearchHistories, key: 'term'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: teamSearchHistories, key: 'term'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'team_id',
findMatchingRecordBy: isRecordTeamSearchHistoryEqualToRaw,
operator: prepareTeamSearchHistoryRecord,
transformer: transformTeamSearchHistoryRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: TEAM_SEARCH_HISTORY,
});
@@ -178,7 +178,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
};
/**
* handleSlashCommand: Handler responsible for the Create/Update operations occurring on the SLASH_COMMAND entity from the 'Server' schema
* handleSlashCommand: Handler responsible for the Create/Update operations occurring on the SLASH_COMMAND table from the 'Server' schema
* @param {HandleSlashCommandArgs} slashCommandsArgs
* @param {RawSlashCommand[]} slashCommandsArgs.slashCommands
* @param {boolean} slashCommandsArgs.prepareRecordsOnly
@@ -194,14 +194,14 @@ const TeamHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({raws: slashCommands, key: 'id'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: slashCommands, key: 'id'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'id',
findMatchingRecordBy: isRecordSlashCommandEqualToRaw,
operator: prepareSlashCommandRecord,
transformer: transformSlashCommandRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: SLASH_COMMAND,
});
@@ -209,7 +209,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
};
/**
* handleMyTeam: Handler responsible for the Create/Update operations occurring on the MY_TEAM entity from the 'Server' schema
* handleMyTeam: Handler responsible for the Create/Update operations occurring on the MY_TEAM table from the 'Server' schema
* @param {HandleMyTeamArgs} myTeamsArgs
* @param {RawMyTeam[]} myTeamsArgs.myTeams
* @param {boolean} myTeamsArgs.prepareRecordsOnly
@@ -225,14 +225,14 @@ const TeamHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({raws: myTeams, key: 'team_id'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: myTeams, key: 'team_id'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'team_id',
findMatchingRecordBy: isRecordMyTeamEqualToRaw,
operator: prepareMyTeamRecord,
transformer: transformMyTeamRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: MY_TEAM,
});

View File

@@ -0,0 +1,222 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import ServerDataOperator from '@database/operator/server_data_operator';
import {
isRecordChannelMembershipEqualToRaw,
isRecordPreferenceEqualToRaw,
isRecordUserEqualToRaw,
} from '@database/operator/server_data_operator/comparators';
import {
transformChannelMembershipRecord,
transformPreferenceRecord,
transformUserRecord,
} from '@database/operator/server_data_operator/transformers/user';
describe('*** Operator: User Handlers tests ***', () => {
let operator: ServerDataOperator;
beforeAll(async () => {
await DatabaseManager.init(['baseHandler.test.com']);
operator = DatabaseManager.serverDatabases['baseHandler.test.com'].operator;
});
it('=> HandleReactions: should write to both Reactions and CustomEmoji tables', async () => {
expect.assertions(2);
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
const spyOnBatchOperation = jest.spyOn(operator, 'batchRecords');
await operator.handleReactions({
reactions: [
{
create_at: 1608263728086,
delete_at: 0,
emoji_name: 'p4p1',
post_id: '4r9jmr7eqt8dxq3f9woypzurry',
update_at: 1608263728077,
user_id: 'ooumoqgq3bfiijzwbn8badznwc',
},
],
prepareRecordsOnly: false,
});
// Called twice: Once for Reaction record and once for CustomEmoji record
expect(spyOnPrepareRecords).toHaveBeenCalledTimes(2);
// Only one batch operation for both tables
expect(spyOnBatchOperation).toHaveBeenCalledTimes(1);
});
it('=> HandleUsers: should write to the User table', async () => {
expect.assertions(2);
const users = [
{
id: '9ciscaqbrpd6d8s68k76xb9bte',
create_at: 1599457495881,
update_at: 1607683720173,
delete_at: 0,
username: 'a.l',
auth_service: 'saml',
email: 'a.l@mattermost.com',
email_verified: true,
is_bot: false,
nickname: '',
first_name: 'A',
last_name: 'L',
position: 'Mobile Engineer',
roles: 'system_user',
props: {},
notify_props: {
desktop: 'all',
desktop_sound: true,
email: true,
first_name: true,
mention_keys: '',
push: 'mention',
channel: true,
auto_responder_active: false,
auto_responder_message: 'Hello, I am out of office and unable to respond to messages.',
comments: 'never',
desktop_notification_sound: 'Hello',
push_status: 'online',
},
last_password_update: 1604323112537,
last_picture_update: 1604686302260,
locale: 'en',
timezone: {
automaticTimezone: 'Indian/Mauritius',
manualTimezone: '',
useAutomaticTimezone: '',
},
},
];
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
await operator.handleUsers({users, prepareRecordsOnly: false});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'id',
createOrUpdateRawValues: users,
tableName: 'User',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordUserEqualToRaw,
transformer: transformUserRecord,
});
});
it('=> HandlePreferences: should write to the PREFERENCE table', async () => {
expect.assertions(2);
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
const preferences = [
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'group_channel_show',
name: 'qj91hepgjfn6xr4acm5xzd8zoc',
value: 'true',
},
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'notifications',
name: 'email_interval',
value: '30',
},
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'theme',
name: '',
value:
'{"awayIndicator":"#c1b966","buttonBg":"#4cbba4","buttonColor":"#ffffff","centerChannelBg":"#2f3e4e","centerChannelColor":"#dddddd","codeTheme":"solarized-dark","dndIndicator":"#e81023","errorTextColor":"#ff6461","image":"/static/files/0b8d56c39baf992e5e4c58d74fde0fd6.png","linkColor":"#a4ffeb","mentionBg":"#b74a4a","mentionColor":"#ffffff","mentionHighlightBg":"#984063","mentionHighlightLink":"#a4ffeb","newMessageSeparator":"#5de5da","onlineIndicator":"#65dcc8","sidebarBg":"#1b2c3e","sidebarHeaderBg":"#1b2c3e","sidebarHeaderTextColor":"#ffffff","sidebarText":"#ffffff","sidebarTextActiveBorder":"#66b9a7","sidebarTextActiveColor":"#ffffff","sidebarTextHoverBg":"#4a5664","sidebarUnreadText":"#ffffff","type":"Mattermost Dark"}',
},
{
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
category: 'tutorial_step',
name: '9ciscaqbrpd6d8s68k76xb9bte',
value: '2',
},
];
await operator.handlePreferences({
preferences,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
createOrUpdateRawValues: preferences,
tableName: 'Preference',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordPreferenceEqualToRaw,
transformer: transformPreferenceRecord,
});
});
it('=> HandleChannelMembership: should write to the CHANNEL_MEMBERSHIP table', async () => {
expect.assertions(2);
const channelMemberships = [
{
channel_id: '17bfnb1uwb8epewp4q3x3rx9go',
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
roles: 'wqyby5r5pinxxdqhoaomtacdhc',
last_viewed_at: 1613667352029,
msg_count: 3864,
mention_count: 0,
notify_props: {
desktop: 'default',
email: 'default',
ignore_channel_mentions: 'default',
mark_unread: 'mention',
push: 'default',
},
last_update_at: 1613667352029,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
{
channel_id: '1yw6gxfr4bn1jbyp9nr7d53yew',
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
roles: 'channel_user',
last_viewed_at: 1615300540549,
msg_count: 16,
mention_count: 0,
notify_props: {
desktop: 'default',
email: 'default',
ignore_channel_mentions: 'default',
mark_unread: 'all',
push: 'default',
},
last_update_at: 1615300540549,
scheme_guest: false,
scheme_user: true,
scheme_admin: false,
explicit_roles: '',
},
];
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
await operator.handleChannelMembership({
channelMemberships,
prepareRecordsOnly: false,
});
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
expect(spyOnHandleRecords).toHaveBeenCalledWith({
fieldName: 'user_id',
createOrUpdateRawValues: channelMemberships,
tableName: 'ChannelMembership',
prepareRecordsOnly: false,
findMatchingRecordBy: isRecordChannelMembershipEqualToRaw,
transformer: transformChannelMembershipRecord,
});
});
});

View File

@@ -7,18 +7,18 @@ import {
isRecordChannelMembershipEqualToRaw,
isRecordPreferenceEqualToRaw,
isRecordUserEqualToRaw,
} from '@database/operator/comparators';
import {prepareCustomEmojiRecord} from '@database/operator/prepareRecords/general';
} from '@database/operator/server_data_operator/comparators';
import {transformCustomEmojiRecord} from '@database/operator/server_data_operator/transformers/general';
import {
prepareChannelMembershipRecord,
preparePreferenceRecord,
prepareReactionRecord,
prepareUserRecord,
} from '@database/operator/prepareRecords/user';
transformChannelMembershipRecord,
transformPreferenceRecord,
transformReactionRecord,
transformUserRecord,
} from '@database/operator/server_data_operator/transformers/user';
import {getRawRecordPairs, getUniqueRawsBy} from '@database/operator/utils/general';
import {sanitizeReactions} from '@database/operator/utils/reaction';
import ChannelMembership from '@typings/database/channel_membership';
import CustomEmoji from '@typings/database/custom_emoji';
import ChannelMembership from '@typings/database/models/servers/channel_membership';
import CustomEmoji from '@typings/database/models/servers/custom_emoji';
import {
HandleChannelMembershipArgs,
HandlePreferencesArgs,
@@ -26,9 +26,9 @@ import {
HandleUsersArgs,
RawReaction,
} from '@typings/database/database';
import Preference from '@typings/database/preference';
import Reaction from '@typings/database/reaction';
import User from '@typings/database/user';
import Preference from '@typings/database/models/servers/preference';
import Reaction from '@typings/database/models/servers/reaction';
import User from '@typings/database/models/servers/user';
const {
CHANNEL_MEMBERSHIP,
@@ -39,20 +39,20 @@ const {
} = MM_TABLES.SERVER;
export interface UserHandlerMix {
handleChannelMembership : ({channelMemberships, prepareRecordsOnly}: HandleChannelMembershipArgs) => ChannelMembership[];
handlePreferences : ({preferences, prepareRecordsOnly}: HandlePreferencesArgs) => Preference[];
handleReactions : ({reactions, prepareRecordsOnly}: HandleReactionsArgs) =>(Reaction | CustomEmoji)[];
handleUsers : ({users, prepareRecordsOnly}: HandleUsersArgs) => User[];
handleChannelMembership : ({channelMemberships, prepareRecordsOnly}: HandleChannelMembershipArgs) => Promise<ChannelMembership[]>;
handlePreferences : ({preferences, prepareRecordsOnly}: HandlePreferencesArgs) => Promise<Preference[]>;
handleReactions : ({reactions, prepareRecordsOnly}: HandleReactionsArgs) => Promise<(Reaction | CustomEmoji)[]>;
handleUsers : ({users, prepareRecordsOnly}: HandleUsersArgs) => Promise<User[]>;
}
const UserHandler = (superclass: any) => class extends superclass {
/**
* handleChannelMembership: Handler responsible for the Create/Update operations occurring on the CHANNEL_MEMBERSHIP entity from the 'Server' schema
* handleChannelMembership: Handler responsible for the Create/Update operations occurring on the CHANNEL_MEMBERSHIP table from the 'Server' schema
* @param {HandleChannelMembershipArgs} channelMembershipsArgs
* @param {RawChannelMembership[]} channelMembershipsArgs.channelMemberships
* @param {boolean} channelMembershipsArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {ChannelMembership[]}
* @returns {Promise<ChannelMembership[]>}
*/
handleChannelMembership = async ({channelMemberships, prepareRecordsOnly = true}: HandleChannelMembershipArgs) => {
let records: ChannelMembership[] = [];
@@ -63,14 +63,14 @@ const UserHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({raws: channelMemberships, key: 'channel_id'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: channelMemberships, key: 'channel_id'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'user_id',
findMatchingRecordBy: isRecordChannelMembershipEqualToRaw,
operator: prepareChannelMembershipRecord,
transformer: transformChannelMembershipRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: CHANNEL_MEMBERSHIP,
});
@@ -78,12 +78,12 @@ const UserHandler = (superclass: any) => class extends superclass {
};
/**
* handlePreferences: Handler responsible for the Create/Update operations occurring on the PREFERENCE entity from the 'Server' schema
* handlePreferences: Handler responsible for the Create/Update operations occurring on the PREFERENCE table from the 'Server' schema
* @param {HandlePreferencesArgs} preferencesArgs
* @param {RawPreference[]} preferencesArgs.preferences
* @param {boolean} preferencesArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {Preference[]}
* @returns {Promise<Preference[]>}
*/
handlePreferences = async ({preferences, prepareRecordsOnly = true}: HandlePreferencesArgs) => {
let records: Preference[] = [];
@@ -94,14 +94,14 @@ const UserHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({raws: preferences, key: 'name'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: preferences, key: 'name'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'user_id',
findMatchingRecordBy: isRecordPreferenceEqualToRaw,
operator: preparePreferenceRecord,
transformer: transformPreferenceRecord,
prepareRecordsOnly,
rawValues,
createOrUpdateRawValues,
tableName: PREFERENCE,
});
@@ -109,12 +109,12 @@ const UserHandler = (superclass: any) => class extends superclass {
};
/**
* handleReactions: Handler responsible for the Create/Update operations occurring on the Reaction entity from the 'Server' schema
* handleReactions: Handler responsible for the Create/Update operations occurring on the Reaction table from the 'Server' schema
* @param {HandleReactionsArgs} handleReactions
* @param {RawReaction[]} handleReactions.reactions
* @param {boolean} handleReactions.prepareRecordsOnly
* @throws DataOperatorException
* @returns {(Reaction| CustomEmoji)[]}
* @returns {Promise<(Reaction| CustomEmoji)[]>}
*/
handleReactions = async ({reactions, prepareRecordsOnly}: HandleReactionsArgs) => {
let batchRecords: (Reaction| CustomEmoji)[] = [];
@@ -127,14 +127,12 @@ const UserHandler = (superclass: any) => class extends superclass {
const rawValues = getUniqueRawsBy({raws: reactions, key: 'emoji_name'}) as RawReaction[];
const database = await this.getDatabase(REACTION);
const {
createEmojis,
createReactions,
deleteReactions,
} = await sanitizeReactions({
database,
database: this.database,
post_id: reactions[0].post_id,
rawReactions: rawValues,
});
@@ -143,8 +141,7 @@ const UserHandler = (superclass: any) => class extends superclass {
// Prepares record for model Reactions
const reactionsRecords = (await this.prepareRecords({
createRaws: createReactions,
database,
recordOperator: prepareReactionRecord,
transformer: transformReactionRecord,
tableName: REACTION,
})) as Reaction[];
batchRecords = batchRecords.concat(reactionsRecords);
@@ -154,8 +151,7 @@ const UserHandler = (superclass: any) => class extends superclass {
// Prepares records for model CustomEmoji
const emojiRecords = (await this.prepareRecords({
createRaws: getRawRecordPairs(createEmojis),
database,
recordOperator: prepareCustomEmojiRecord,
transformer: transformCustomEmojiRecord,
tableName: CUSTOM_EMOJI,
})) as CustomEmoji[];
batchRecords = batchRecords.concat(emojiRecords);
@@ -168,22 +164,19 @@ const UserHandler = (superclass: any) => class extends superclass {
}
if (batchRecords?.length) {
await this.batchOperations({
database,
models: batchRecords,
});
await this.batchRecords(batchRecords);
}
return [];
};
/**
* handleUsers: Handler responsible for the Create/Update operations occurring on the User entity from the 'Server' schema
* handleUsers: Handler responsible for the Create/Update operations occurring on the User table from the 'Server' schema
* @param {HandleUsersArgs} usersArgs
* @param {RawUser[]} usersArgs.users
* @param {boolean} usersArgs.prepareRecordsOnly
* @throws DataOperatorException
* @returns {User[]}
* @returns {Promise<User[]>}
*/
handleUsers = async ({users, prepareRecordsOnly = true}: HandleUsersArgs) => {
let records: User[] = [];
@@ -194,13 +187,13 @@ const UserHandler = (superclass: any) => class extends superclass {
);
}
const rawValues = getUniqueRawsBy({raws: users, key: 'id'});
const createOrUpdateRawValues = getUniqueRawsBy({raws: users, key: 'id'});
records = await this.handleEntityRecords({
records = await this.handleRecords({
fieldName: 'id',
findMatchingRecordBy: isRecordUserEqualToRaw,
operator: prepareUserRecord,
rawValues,
transformer: transformUserRecord,
createOrUpdateRawValues,
tableName: USER,
prepareRecordsOnly,
});

View File

@@ -0,0 +1,29 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import ServerDataOperatorBase from '@database/operator/server_data_operator/handlers';
import ChannelHandler, {ChannelHandlerMix} from '@database/operator/server_data_operator/handlers/channel';
import GroupHandler, {GroupHandlerMix} from '@database/operator/server_data_operator/handlers/group';
import PostHandler, {PostHandlerMix} from '@database/operator/server_data_operator/handlers/post';
import TeamHandler, {TeamHandlerMix} from '@database/operator/server_data_operator/handlers/team';
import UserHandler, {UserHandlerMix} from '@database/operator/server_data_operator/handlers/user';
import mix from '@utils/mix';
import type {Database} from '@nozbe/watermelondb';
interface ServerDataOperator extends ServerDataOperatorBase, PostHandlerMix, UserHandlerMix, GroupHandlerMix, ChannelHandlerMix, TeamHandlerMix {}
class ServerDataOperator extends mix(ServerDataOperatorBase).with(
ChannelHandler,
GroupHandler,
PostHandler,
TeamHandler,
UserHandler,
) {
// eslint-disable-next-line no-useless-constructor
constructor(database: Database) {
super(database);
}
}
export default ServerDataOperator;

View File

@@ -2,22 +2,22 @@
// See LICENSE.txt for license information.
import {
prepareChannelInfoRecord,
prepareChannelRecord,
prepareMyChannelRecord,
prepareMyChannelSettingsRecord,
} from '@database/operator/prepareRecords/channel';
transformChannelInfoRecord,
transformChannelRecord,
transformMyChannelRecord,
transformMyChannelSettingsRecord,
} from '@database/operator/server_data_operator/transformers/channel';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
describe('*** CHANNEL Prepare Records Test ***', () => {
it('=> prepareChannelRecord: should return an array of type Channel', async () => {
it('=> transformChannelRecord: should return an array of type Channel', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'channel_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareChannelRecord({
const preparedRecords = await transformChannelRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -49,13 +49,13 @@ describe('*** CHANNEL Prepare Records Test ***', () => {
expect(preparedRecords.collection.modelClass.name).toBe('Channel');
});
it('=> prepareMyChannelSettingsRecord: should return an array of type MyChannelSettings', async () => {
it('=> transformMyChannelSettingsRecord: should return an array of type MyChannelSettings', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'channel_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareMyChannelSettingsRecord({
const preparedRecords = await transformMyChannelSettingsRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -79,13 +79,13 @@ describe('*** CHANNEL Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('MyChannelSettings');
});
it('=> prepareChannelInfoRecord: should return an array of type ChannelInfo', async () => {
it('=> transformChannelInfoRecord: should return an array of type ChannelInfo', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'channel_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareChannelInfoRecord({
const preparedRecords = await transformChannelInfoRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -105,13 +105,13 @@ describe('*** CHANNEL Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('ChannelInfo');
});
it('=> prepareMyChannelRecord: should return an array of type MyChannel', async () => {
it('=> transformMyChannelRecord: should return an array of type MyChannel', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'channel_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareMyChannelRecord({
const preparedRecords = await transformMyChannelRecord({
action: OperationType.CREATE,
database: database!,
value: {

View File

@@ -2,19 +2,19 @@
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/prepareRecords/index';
import Channel from '@typings/database/channel';
import ChannelInfo from '@typings/database/channel_info';
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
import Channel from '@typings/database/models/servers/channel';
import ChannelInfo from '@typings/database/models/servers/channel_info';
import {
DataFactoryArgs,
TransformerArgs,
RawChannel,
RawChannelInfo,
RawMyChannel,
RawMyChannelSettings,
} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
import MyChannel from '@typings/database/my_channel';
import MyChannelSettings from '@typings/database/my_channel_settings';
import MyChannel from '@typings/database/models/servers/my_channel';
import MyChannelSettings from '@typings/database/models/servers/my_channel_settings';
const {
CHANNEL,
@@ -24,19 +24,19 @@ const {
} = MM_TABLES.SERVER;
/**
* prepareChannelRecord: Prepares record of entity 'CHANNEL' from the SERVER database for update or create actions.
* transformChannelRecord: Prepares a record of the SERVER database 'Channel' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareChannelRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformChannelRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawChannel;
const record = value.record as Channel;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const generator = (channel: Channel) => {
const fieldsMapper = (channel: Channel) => {
channel._raw.id = isCreateAction ? (raw?.id ?? channel.id) : record.id;
channel.createAt = raw.create_at;
channel.creatorId = raw.creator_id;
@@ -53,23 +53,23 @@ export const prepareChannelRecord = ({action, database, value}: DataFactoryArgs)
database,
tableName: CHANNEL,
value,
generator,
fieldsMapper,
});
};
/**
* prepareMyChannelSettingsRecord: Prepares record of entity 'MY_CHANNEL_SETTINGS' from the SERVER database for update or create actions.
* transformMyChannelSettingsRecord: Prepares a record of the SERVER database 'MyChannelSettings' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareMyChannelSettingsRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformMyChannelSettingsRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawMyChannelSettings;
const record = value.record as MyChannelSettings;
const isCreateAction = action === OperationType.CREATE;
const generator = (myChannelSetting: MyChannelSettings) => {
const fieldsMapper = (myChannelSetting: MyChannelSettings) => {
myChannelSetting._raw.id = isCreateAction ? myChannelSetting.id : record.id;
myChannelSetting.channelId = raw.channel_id;
myChannelSetting.notifyProps = raw.notify_props;
@@ -80,29 +80,29 @@ export const prepareMyChannelSettingsRecord = ({action, database, value}: DataFa
database,
tableName: MY_CHANNEL_SETTINGS,
value,
generator,
fieldsMapper,
});
};
/**
* prepareChannelInfoRecord: Prepares record of entity 'CHANNEL_INFO' from the SERVER database for update or create actions.
* transformChannelInfoRecord: Prepares a record of the SERVER database 'ChannelInfo' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareChannelInfoRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformChannelInfoRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawChannelInfo;
const record = value.record as ChannelInfo;
const isCreateAction = action === OperationType.CREATE;
const generator = (channelInfo: ChannelInfo) => {
const fieldsMapper = (channelInfo: ChannelInfo) => {
channelInfo._raw.id = isCreateAction ? channelInfo.id : record.id;
channelInfo.channelId = raw.channel_id;
channelInfo.guestCount = raw.guest_count;
channelInfo.header = raw.header;
channelInfo.memberCount = raw.member_count;
channelInfo.pinned_post_count = raw.pinned_post_count;
channelInfo.pinnedPostCount = raw.pinned_post_count;
channelInfo.purpose = raw.purpose;
};
@@ -111,23 +111,23 @@ export const prepareChannelInfoRecord = ({action, database, value}: DataFactoryA
database,
tableName: CHANNEL_INFO,
value,
generator,
fieldsMapper,
});
};
/**
* prepareMyChannelRecord: Prepares record of entity 'MY_CHANNEL' from the SERVER database for update or create actions.
* transformMyChannelRecord: Prepares a record of the SERVER database 'MyChannel' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareMyChannelRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformMyChannelRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawMyChannel;
const record = value.record as MyChannel;
const isCreateAction = action === OperationType.CREATE;
const generator = (myChannel: MyChannel) => {
const fieldsMapper = (myChannel: MyChannel) => {
myChannel._raw.id = isCreateAction ? myChannel.id : record.id;
myChannel.channelId = raw.channel_id;
myChannel.roles = raw.roles;
@@ -142,7 +142,7 @@ export const prepareMyChannelRecord = ({action, database, value}: DataFactoryArg
database,
tableName: MY_CHANNEL,
value,
generator,
fieldsMapper,
});
};

View File

@@ -0,0 +1,113 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
transformCustomEmojiRecord,
transformRoleRecord,
transformSystemRecord,
transformTermsOfServiceRecord,
} from '@database/operator/server_data_operator/transformers/general';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
describe('*** Role Prepare Records Test ***', () => {
it('=> transformRoleRecord: should return an array of type Role', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'isolated_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await transformRoleRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'role-1',
name: 'role-name-1',
permissions: [],
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('Role');
});
});
describe('*** System Prepare Records Test ***', () => {
it('=> transformSystemRecord: should return an array of type System', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'isolated_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await transformSystemRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {id: 'system-1', name: 'system-name-1', value: 'system'},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('System');
});
});
describe('*** TOS Prepare Records Test ***', () => {
it('=> transformTermsOfServiceRecord: should return an array of type TermsOfService', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'isolated_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await transformTermsOfServiceRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'tos-1',
accepted_at: 1,
create_at: 1613667352029,
user_id: 'user1613667352029',
text: '',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('TermsOfService');
});
});
describe('*** CustomEmoj Prepare Records Test ***', () => {
it('=> transformCustomEmojiRecord: should return an array of type CustomEmoji', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'isolated_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await transformCustomEmojiRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'i',
create_at: 1580913641769,
update_at: 1580913641769,
delete_at: 0,
creator_id: '4cprpki7ri81mbx8efixcsb8jo',
name: 'boomI',
},
},
});
expect(preparedRecords).toBeTruthy();
expect(preparedRecords!.collection.modelClass.name).toBe('CustomEmoji');
});
});

View File

@@ -0,0 +1,131 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
import CustomEmoji from '@typings/database/models/servers/custom_emoji';
import {
TransformerArgs,
RawCustomEmoji,
RawRole,
RawSystem,
RawTermsOfService,
} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
import Role from '@typings/database/models/servers/role';
import System from '@typings/database/models/servers/system';
import TermsOfService from '@typings/database/models/servers/terms_of_service';
const {
CUSTOM_EMOJI,
ROLE,
SYSTEM,
TERMS_OF_SERVICE,
} = MM_TABLES.SERVER;
/**
* transformCustomEmojiRecord: Prepares a record of the SERVER database 'CustomEmoji' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformCustomEmojiRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawCustomEmoji;
const record = value.record as CustomEmoji;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const fieldsMapper = (emoji: CustomEmoji) => {
emoji._raw.id = isCreateAction ? (raw?.id ?? emoji.id) : record.id;
emoji.name = raw.name;
};
return prepareBaseRecord({
action,
database,
tableName: CUSTOM_EMOJI,
value,
fieldsMapper,
});
};
/**
* transformRoleRecord: Prepares a record of the SERVER database 'Role' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformRoleRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawRole;
const record = value.record as Role;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const fieldsMapper = (role: Role) => {
role._raw.id = isCreateAction ? (raw?.id ?? role.id) : record.id;
role.name = raw?.name;
role.permissions = raw?.permissions;
};
return prepareBaseRecord({
action,
database,
tableName: ROLE,
value,
fieldsMapper,
});
};
/**
* transformSystemRecord: Prepares a record of the SERVER database 'System' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformSystemRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawSystem;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const fieldsMapper = (system: System) => {
system.name = raw?.name;
system.value = raw?.value;
};
return prepareBaseRecord({
action,
database,
tableName: SYSTEM,
value,
fieldsMapper,
});
};
/**
* transformTermsOfServiceRecord: Prepares a record of the SERVER database 'TermsOfService' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const transformTermsOfServiceRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawTermsOfService;
const record = value.record as TermsOfService;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const fieldsMapper = (tos: TermsOfService) => {
tos._raw.id = isCreateAction ? (raw?.id ?? tos.id) : record.id;
tos.acceptedAt = raw?.accepted_at;
};
return prepareBaseRecord({
action,
database,
tableName: TERMS_OF_SERVICE,
value,
fieldsMapper,
});
};

View File

@@ -2,22 +2,22 @@
// See LICENSE.txt for license information.
import {
prepareGroupMembershipRecord,
prepareGroupRecord,
prepareGroupsInChannelRecord,
prepareGroupsInTeamRecord,
} from '@database/operator/prepareRecords/group';
transformGroupMembershipRecord,
transformGroupRecord,
transformGroupsInChannelRecord,
transformGroupsInTeamRecord,
} from '@database/operator/server_data_operator/transformers/group';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
describe('*** GROUP Prepare Records Test ***', () => {
it('=> prepareGroupRecord: should return an array of type Group', async () => {
it('=> transformGroupRecord: should return an array of type Group', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'group_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareGroupRecord({
const preparedRecords = await transformGroupRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -41,13 +41,13 @@ describe('*** GROUP Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('Group');
});
it('=> prepareGroupsInTeamRecord: should return an array of type GroupsInTeam', async () => {
it('=> transformGroupsInTeamRecord: should return an array of type GroupsInTeam', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'group_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareGroupsInTeamRecord({
const preparedRecords = await transformGroupsInTeamRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -69,13 +69,13 @@ describe('*** GROUP Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('GroupsInTeam');
});
it('=> prepareGroupsInChannelRecord: should return an array of type GroupsInChannel', async () => {
it('=> transformGroupsInChannelRecord: should return an array of type GroupsInChannel', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'group_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareGroupsInChannelRecord({
const preparedRecords = await transformGroupsInChannelRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -100,13 +100,13 @@ describe('*** GROUP Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('GroupsInChannel');
});
it('=> prepareGroupMembershipRecord: should return an array of type GroupMembership', async () => {
it('=> transformGroupMembershipRecord: should return an array of type GroupMembership', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'group_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareGroupMembershipRecord({
const preparedRecords = await transformGroupMembershipRecord({
action: OperationType.CREATE,
database: database!,
value: {

View File

@@ -2,19 +2,19 @@
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/prepareRecords/index';
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
import {
DataFactoryArgs,
TransformerArgs,
RawGroup,
RawGroupMembership,
RawGroupsInChannel,
RawGroupsInTeam,
} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
import Group from '@typings/database/group';
import GroupMembership from '@typings/database/group_membership';
import GroupsInChannel from '@typings/database/groups_in_channel';
import GroupsInTeam from '@typings/database/groups_in_team';
import Group from '@typings/database/models/servers/group';
import GroupMembership from '@typings/database/models/servers/group_membership';
import GroupsInChannel from '@typings/database/models/servers/groups_in_channel';
import GroupsInTeam from '@typings/database/models/servers/groups_in_team';
const {
GROUP,
@@ -24,19 +24,19 @@ const {
} = MM_TABLES.SERVER;
/**
* prepareGroupMembershipRecord: Prepares record of entity 'GROUP_MEMBERSHIP' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* transformGroupMembershipRecord: Prepares a record of the SERVER database 'GroupMembership' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareGroupMembershipRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformGroupMembershipRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawGroupMembership;
const record = value.record as GroupMembership;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const generator = (groupMember: GroupMembership) => {
const fieldsMapper = (groupMember: GroupMembership) => {
groupMember._raw.id = isCreateAction ? (raw?.id ?? groupMember.id) : record.id;
groupMember.groupId = raw.group_id;
groupMember.userId = raw.user_id;
@@ -47,24 +47,24 @@ export const prepareGroupMembershipRecord = ({action, database, value}: DataFact
database,
tableName: GROUP_MEMBERSHIP,
value,
generator,
fieldsMapper,
});
};
/**
* prepareGroupRecord: Prepares record of entity 'GROUP' from the SERVER database for update or create actions.
* transformGroupRecord: Prepares a record of the SERVER database 'Group' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareGroupRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformGroupRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawGroup;
const record = value.record as Group;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const generator = (group: Group) => {
const fieldsMapper = (group: Group) => {
group._raw.id = isCreateAction ? (raw?.id ?? group.id) : record.id;
group.name = raw.name;
group.displayName = raw.display_name;
@@ -75,23 +75,23 @@ export const prepareGroupRecord = ({action, database, value}: DataFactoryArgs) =
database,
tableName: GROUP,
value,
generator,
fieldsMapper,
});
};
/**
* prepareGroupsInTeamRecord: Prepares record of entity 'GROUPS_IN_TEAM' from the SERVER database for update or create actions.
* transformGroupsInTeamRecord: Prepares a record of the SERVER database 'GroupsInTeam' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareGroupsInTeamRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformGroupsInTeamRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawGroupsInTeam;
const record = value.record as GroupsInTeam;
const isCreateAction = action === OperationType.CREATE;
const generator = (groupsInTeam: GroupsInTeam) => {
const fieldsMapper = (groupsInTeam: GroupsInTeam) => {
groupsInTeam._raw.id = isCreateAction ? groupsInTeam.id : record.id;
groupsInTeam.teamId = raw.team_id;
groupsInTeam.groupId = raw.group_id;
@@ -102,23 +102,23 @@ export const prepareGroupsInTeamRecord = ({action, database, value}: DataFactory
database,
tableName: GROUPS_IN_TEAM,
value,
generator,
fieldsMapper,
});
};
/**
* prepareGroupsInChannelRecord: Prepares record of entity 'GROUPS_IN_CHANNEL' from the SERVER database for update or create actions.
* transformGroupsInChannelRecord: Prepares a record of the SERVER database 'GroupsInChannel' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareGroupsInChannelRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformGroupsInChannelRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawGroupsInChannel;
const record = value.record as GroupsInChannel;
const isCreateAction = action === OperationType.CREATE;
const generator = (groupsInChannel: GroupsInChannel) => {
const fieldsMapper = (groupsInChannel: GroupsInChannel) => {
groupsInChannel._raw.id = isCreateAction ? groupsInChannel.id : record.id;
groupsInChannel.channelId = raw.channel_id;
groupsInChannel.groupId = raw.group_id;
@@ -131,6 +131,6 @@ export const prepareGroupsInChannelRecord = ({action, database, value}: DataFact
database,
tableName: GROUPS_IN_CHANNEL,
value,
generator,
fieldsMapper,
});
};

View File

@@ -2,18 +2,18 @@
// See LICENSE.txt for license information.
import Model from '@nozbe/watermelondb/Model';
import {DataFactoryArgs} from '@typings/database/database';
import {TransformerArgs} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
/**
* prepareBaseRecord: This is the last step for each operator and depending on the 'action', it will either prepare an
* existing record for UPDATE or prepare a collection for CREATE
*
* @param {DataFactoryArgs} operatorBase
* @param {TransformerArgs} operatorBase
* @param {Database} operatorBase.database
* @param {string} operatorBase.tableName
* @param {RecordPair} operatorBase.value
* @param {((DataFactoryArgs) => void)} operatorBase.generator
* @param {((TransformerArgs) => void)} operatorBase.generator
* @returns {Promise<Model>}
*/
export const prepareBaseRecord = async ({
@@ -21,12 +21,12 @@ export const prepareBaseRecord = async ({
database,
tableName,
value,
generator,
}: DataFactoryArgs): Promise<Model> => {
fieldsMapper,
}: TransformerArgs): Promise<Model> => {
if (action === OperationType.UPDATE) {
const record = value.record as Model;
return record.prepareUpdate(() => generator!(record));
return record.prepareUpdate(() => fieldsMapper!(record));
}
return database.collections.get(tableName!).prepareCreate(generator);
return database.collections.get(tableName!).prepareCreate(fieldsMapper);
};

View File

@@ -2,26 +2,24 @@
// See LICENSE.txt for license information.
import {
prepareDraftRecord,
prepareFileRecord,
preparePostInThreadRecord,
preparePostMetadataRecord,
preparePostRecord,
preparePostsInChannelRecord,
} from '@database/operator/prepareRecords/post';
transformDraftRecord,
transformFileRecord,
transformPostInThreadRecord,
transformPostMetadataRecord,
transformPostRecord,
transformPostsInChannelRecord,
} from '@database/operator/server_data_operator/transformers/post';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** POST Prepare Records Test ***', () => {
it('=> preparePostRecord: should return an array of type Post', async () => {
it('=> transformPostRecord: should return an array of type Post', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'post_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await preparePostRecord({
const preparedRecords = await transformPostRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -38,7 +36,7 @@ describe('*** POST Prepare Records Test ***', () => {
root_id: 'ps81iqbesfby8jayz7owg4yypoo',
parent_id: 'ps81iqbddesfby8jayz7owg4yypoo',
original_id: '',
message: 'Testing operator post',
message: 'Testing composer post',
type: '',
props: {},
hashtags: '',
@@ -55,13 +53,13 @@ describe('*** POST Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('Post');
});
it('=> preparePostInThreadRecord: should return an array of type PostsInThread', async () => {
it('=> transformPostInThreadRecord: should return an array of type PostsInThread', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'post_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await preparePostInThreadRecord({
const preparedRecords = await transformPostInThreadRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -81,13 +79,13 @@ describe('*** POST Prepare Records Test ***', () => {
);
});
it('=> prepareFileRecord: should return an array of type File', async () => {
it('=> transformFileRecord: should return an array of type File', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'post_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareFileRecord({
const preparedRecords = await transformFileRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -110,13 +108,13 @@ describe('*** POST Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('File');
});
it('=> preparePostMetadataRecord: should return an array of type PostMetadata', async () => {
it('=> transformPostMetadataRecord: should return an array of type PostMetadata', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'post_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await preparePostMetadataRecord({
const preparedRecords = await transformPostMetadataRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -134,13 +132,13 @@ describe('*** POST Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('PostMetadata');
});
it('=> prepareDraftRecord: should return an array of type Draft', async () => {
it('=> transformDraftRecord: should return an array of type Draft', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'post_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareDraftRecord({
const preparedRecords = await transformDraftRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -159,13 +157,13 @@ describe('*** POST Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('Draft');
});
it('=> preparePostsInChannelRecord: should return an array of type PostsInChannel', async () => {
it('=> transformPostsInChannelRecord: should return an array of type PostsInChannel', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'post_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await preparePostsInChannelRecord({
const preparedRecords = await transformPostsInChannelRecord({
action: OperationType.CREATE,
database: database!,
value: {

View File

@@ -1,11 +1,11 @@
// 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 {prepareBaseRecord} from '@database/operator/prepareRecords/index';
import {Q} from '@nozbe/watermelondb';
import {
DataFactoryArgs,
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
import type{
TransformerArgs,
RawDraft,
RawFile,
RawPost,
@@ -13,13 +13,13 @@ import {
RawPostsInChannel,
RawPostsInThread,
} from '@typings/database/database';
import Draft from '@typings/database/draft';
import Draft from '@typings/database/models/servers/draft';
import {OperationType} from '@typings/database/enums';
import File from '@typings/database/file';
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 File from '@typings/database/models/servers/file';
import Post from '@typings/database/models/servers/post';
import PostMetadata from '@typings/database/models/servers/post_metadata';
import PostsInChannel from '@typings/database/models/servers/posts_in_channel';
import PostsInThread from '@typings/database/models/servers/posts_in_thread';
const {
DRAFT,
@@ -31,19 +31,19 @@ const {
} = MM_TABLES.SERVER;
/**
* preparePostRecord: Prepares record of entity 'Post' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* transformPostRecord: Prepares a record of the SERVER database 'Post' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const preparePostRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformPostRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawPost;
const record = value.record as Post;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const generator = (post: Post) => {
const fieldsMapper = (post: Post) => {
post._raw.id = isCreateAction ? (raw?.id ?? post.id) : record.id;
post.channelId = raw.channel_id;
post.createAt = raw.create_at;
@@ -66,23 +66,23 @@ export const preparePostRecord = ({action, database, value}: DataFactoryArgs) =>
database,
tableName: POST,
value,
generator,
fieldsMapper,
});
};
/**
* preparePostInThreadRecord: Prepares record of entity 'POSTS_IN_THREAD' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* transformPostInThreadRecord: Prepares a record of the SERVER database 'PostsInThread' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const preparePostInThreadRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformPostInThreadRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawPostsInThread;
const record = value.record as PostsInThread;
const isCreateAction = action === OperationType.CREATE;
const generator = (postsInThread: PostsInThread) => {
const fieldsMapper = (postsInThread: PostsInThread) => {
postsInThread.postId = isCreateAction ? raw.post_id : record.id;
postsInThread.earliest = raw.earliest;
postsInThread.latest = raw.latest!;
@@ -93,24 +93,24 @@ export const preparePostInThreadRecord = ({action, database, value}: DataFactory
database,
tableName: POSTS_IN_THREAD,
value,
generator,
fieldsMapper,
});
};
/**
* prepareFileRecord: Prepares record of entity 'FILE' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* transformFileRecord: Prepares a record of the SERVER database 'Files' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareFileRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformFileRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawFile;
const record = value.record as File;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const generator = (file: File) => {
const fieldsMapper = (file: File) => {
file._raw.id = isCreateAction ? (raw?.id ?? file.id) : record.id;
file.postId = raw.post_id;
file.name = raw.name;
@@ -128,23 +128,23 @@ export const prepareFileRecord = ({action, database, value}: DataFactoryArgs) =>
database,
tableName: FILE,
value,
generator,
fieldsMapper,
});
};
/**
* preparePostMetadataRecord: Prepares record of entity 'POST_METADATA' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* transformPostMetadataRecord: Prepares a record of the SERVER database 'PostMetadata' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const preparePostMetadataRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformPostMetadataRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawPostMetadata;
const record = value.record as PostMetadata;
const isCreateAction = action === OperationType.CREATE;
const generator = (postMeta: PostMetadata) => {
const fieldsMapper = (postMeta: PostMetadata) => {
postMeta._raw.id = isCreateAction ? postMeta.id : record.id;
postMeta.data = raw.data;
postMeta.postId = raw.postId;
@@ -156,23 +156,23 @@ export const preparePostMetadataRecord = ({action, database, value}: DataFactory
database,
tableName: POST_METADATA,
value,
generator,
fieldsMapper,
});
};
/**
* prepareDraftRecord: Prepares record of entity 'DRAFT' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* transformDraftRecord: Prepares a record of the SERVER database 'Draft' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareDraftRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformDraftRecord = ({action, database, value}: TransformerArgs) => {
const emptyFileInfo: FileInfo[] = [];
const raw = value.raw as RawDraft;
// We use the raw id as Draft is client side only and we would only be creating/deleting drafts
const generator = (draft: Draft) => {
const fieldsMapper = (draft: Draft) => {
draft._raw.id = draft.id;
draft.rootId = raw?.root_id ?? '';
draft.message = raw?.message ?? '';
@@ -185,23 +185,23 @@ export const prepareDraftRecord = ({action, database, value}: DataFactoryArgs) =
database,
tableName: DRAFT,
value,
generator,
fieldsMapper,
});
};
/**
* preparePostsInChannelRecord: Prepares record of entity 'POSTS_IN_CHANNEL' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* transformPostsInChannelRecord: Prepares a record of the SERVER database 'PostsInChannel' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const preparePostsInChannelRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformPostsInChannelRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawPostsInChannel;
const record = value.record as PostsInChannel;
const isCreateAction = action === OperationType.CREATE;
const generator = (postsInChannel: PostsInChannel) => {
const fieldsMapper = (postsInChannel: PostsInChannel) => {
postsInChannel._raw.id = isCreateAction ? postsInChannel.id : record.id;
postsInChannel.channelId = raw.channel_id;
postsInChannel.earliest = raw.earliest;
@@ -213,6 +213,6 @@ export const preparePostsInChannelRecord = ({action, database, value}: DataFacto
database,
tableName: POSTS_IN_CHANNEL,
value,
generator,
fieldsMapper,
});
};

View File

@@ -2,24 +2,24 @@
// See LICENSE.txt for license information.
import {
prepareMyTeamRecord,
prepareSlashCommandRecord,
prepareTeamChannelHistoryRecord,
prepareTeamMembershipRecord,
prepareTeamRecord,
prepareTeamSearchHistoryRecord,
} from '@database/operator/prepareRecords/team';
transformMyTeamRecord,
transformSlashCommandRecord,
transformTeamChannelHistoryRecord,
transformTeamMembershipRecord,
transformTeamRecord,
transformTeamSearchHistoryRecord,
} from '@database/operator/server_data_operator/transformers/team';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
describe('*** TEAM Prepare Records Test ***', () => {
it('=> prepareSlashCommandRecord: should return an array of type SlashCommand', async () => {
it('=> transformSlashCommandRecord: should return an array of type SlashCommand', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'team_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareSlashCommandRecord({
const preparedRecords = await transformSlashCommandRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -50,13 +50,13 @@ describe('*** TEAM Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('SlashCommand');
});
it('=> prepareMyTeamRecord: should return an array of type MyTeam', async () => {
it('=> transformMyTeamRecord: should return an array of type MyTeam', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'team_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareMyTeamRecord({
const preparedRecords = await transformMyTeamRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -74,13 +74,13 @@ describe('*** TEAM Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('MyTeam');
});
it('=> prepareTeamRecord: should return an array of type Team', async () => {
it('=> transformTeamRecord: should return an array of type Team', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'team_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareTeamRecord({
const preparedRecords = await transformTeamRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -110,13 +110,13 @@ describe('*** TEAM Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('Team');
});
it('=> prepareTeamChannelHistoryRecord: should return an array of type Team', async () => {
it('=> transformTeamChannelHistoryRecord: should return an array of type Team', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'team_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareTeamChannelHistoryRecord({
const preparedRecords = await transformTeamChannelHistoryRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -132,13 +132,13 @@ describe('*** TEAM Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('TeamChannelHistory');
});
it('=> prepareTeamSearchHistoryRecord: should return an array of type TeamSearchHistory', async () => {
it('=> transformTeamSearchHistoryRecord: should return an array of type TeamSearchHistory', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'team_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareTeamSearchHistoryRecord({
const preparedRecords = await transformTeamSearchHistoryRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -156,13 +156,13 @@ describe('*** TEAM Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('TeamSearchHistory');
});
it('=> prepareTeamMembershipRecord: should return an array of type TeamMembership', async () => {
it('=> transformTeamMembershipRecord: should return an array of type TeamMembership', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'team_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareTeamMembershipRecord({
const preparedRecords = await transformTeamMembershipRecord({
action: OperationType.CREATE,
database: database!,
value: {

View File

@@ -1,11 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
// See LICENSE.txt for license information.
import {prepareBaseRecord} from '@database/operator/prepareRecords/index';
import {
DataFactoryArgs,
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
import type {
TransformerArgs,
RawMyTeam,
RawSlashCommand,
RawTeam,
@@ -14,12 +14,12 @@ import {
RawTeamSearchHistory,
} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
import MyTeam from '@typings/database/my_team';
import SlashCommand from '@typings/database/slash_command';
import Team from '@typings/database/team';
import TeamChannelHistory from '@typings/database/team_channel_history';
import TeamMembership from '@typings/database/team_membership';
import TeamSearchHistory from '@typings/database/team_search_history';
import MyTeam from '@typings/database/models/servers/my_team';
import SlashCommand from '@typings/database/models/servers/slash_command';
import Team from '@typings/database/models/servers/team';
import TeamChannelHistory from '@typings/database/models/servers/team_channel_history';
import TeamMembership from '@typings/database/models/servers/team_membership';
import TeamSearchHistory from '@typings/database/models/servers/team_search_history';
const {
MY_TEAM,
@@ -31,19 +31,19 @@ const {
} = MM_TABLES.SERVER;
/**
* preparePreferenceRecord: Prepares record of entity 'TEAM_MEMBERSHIP' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* transformTeamMembershipRecord: Prepares a record of the SERVER database 'TeamMembership' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareTeamMembershipRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformTeamMembershipRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawTeamMembership;
const record = value.record as TeamMembership;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const generator = (teamMembership: TeamMembership) => {
const fieldsMapper = (teamMembership: TeamMembership) => {
teamMembership._raw.id = isCreateAction ? (raw?.id ?? teamMembership.id) : record.id;
teamMembership.teamId = raw.team_id;
teamMembership.userId = raw.user_id;
@@ -54,24 +54,24 @@ export const prepareTeamMembershipRecord = ({action, database, value}: DataFacto
database,
tableName: TEAM_MEMBERSHIP,
value,
generator,
fieldsMapper,
});
};
/**
* prepareTeamRecord: Prepares record of entity 'TEAM' from the SERVER database for update or create actions.
* transformTeamRecord: Prepares a record of the SERVER database 'Team' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareTeamRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformTeamRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawTeam;
const record = value.record as Team;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const generator = (team: Team) => {
const fieldsMapper = (team: Team) => {
team._raw.id = isCreateAction ? (raw?.id ?? team.id) : record.id;
team.isAllowOpenInvite = raw.allow_open_invite;
team.description = raw.description;
@@ -89,23 +89,23 @@ export const prepareTeamRecord = ({action, database, value}: DataFactoryArgs) =>
database,
tableName: TEAM,
value,
generator,
fieldsMapper,
});
};
/**
* prepareTeamChannelHistoryRecord: Prepares record of entity 'TEAM_CHANNEL_HISTORY' from the SERVER database for update or create actions.
* transformTeamChannelHistoryRecord: Prepares a record of the SERVER database 'TeamChannelHistory' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareTeamChannelHistoryRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformTeamChannelHistoryRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawTeamChannelHistory;
const record = value.record as TeamChannelHistory;
const isCreateAction = action === OperationType.CREATE;
const generator = (teamChannelHistory: TeamChannelHistory) => {
const fieldsMapper = (teamChannelHistory: TeamChannelHistory) => {
teamChannelHistory._raw.id = isCreateAction ? (teamChannelHistory.id) : record.id;
teamChannelHistory.teamId = raw.team_id;
teamChannelHistory.channelIds = raw.channel_ids;
@@ -116,23 +116,23 @@ export const prepareTeamChannelHistoryRecord = ({action, database, value}: DataF
database,
tableName: TEAM_CHANNEL_HISTORY,
value,
generator,
fieldsMapper,
});
};
/**
* prepareTeamSearchHistoryRecord: Prepares record of entity 'TEAM_SEARCH_HISTORY' from the SERVER database for update or create actions.
* transformTeamSearchHistoryRecord: Prepares a record of the SERVER database 'TeamSearchHistory' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareTeamSearchHistoryRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformTeamSearchHistoryRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawTeamSearchHistory;
const record = value.record as TeamSearchHistory;
const isCreateAction = action === OperationType.CREATE;
const generator = (teamSearchHistory: TeamSearchHistory) => {
const fieldsMapper = (teamSearchHistory: TeamSearchHistory) => {
teamSearchHistory._raw.id = isCreateAction ? (teamSearchHistory.id) : record.id;
teamSearchHistory.createdAt = raw.created_at;
teamSearchHistory.displayTerm = raw.display_term;
@@ -145,24 +145,24 @@ export const prepareTeamSearchHistoryRecord = ({action, database, value}: DataFa
database,
tableName: TEAM_SEARCH_HISTORY,
value,
generator,
fieldsMapper,
});
};
/**
* prepareSlashCommandRecord: Prepares record of entity 'SLASH_COMMAND' from the SERVER database for update or create actions.
* transformSlashCommandRecord: Prepares a record of the SERVER database 'SlashCommand' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareSlashCommandRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformSlashCommandRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawSlashCommand;
const record = value.record as SlashCommand;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const generator = (slashCommand: SlashCommand) => {
const fieldsMapper = (slashCommand: SlashCommand) => {
slashCommand._raw.id = isCreateAction ? (raw?.id ?? slashCommand.id) : record.id;
slashCommand.isAutoComplete = raw.auto_complete;
slashCommand.description = raw.description;
@@ -180,23 +180,23 @@ export const prepareSlashCommandRecord = ({action, database, value}: DataFactory
database,
tableName: SLASH_COMMAND,
value,
generator,
fieldsMapper,
});
};
/**
* prepareMyTeamRecord: Prepares record of entity 'MY_TEAM' from the SERVER database for update or create actions.
* transformMyTeamRecord: Prepares a record of the SERVER database 'MyTeam' table for update or create actions.
* @param {DataFactory} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareMyTeamRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformMyTeamRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawMyTeam;
const record = value.record as MyTeam;
const isCreateAction = action === OperationType.CREATE;
const generator = (myTeam: MyTeam) => {
const fieldsMapper = (myTeam: MyTeam) => {
myTeam._raw.id = isCreateAction ? myTeam.id : record.id;
myTeam.teamId = raw.team_id;
myTeam.roles = raw.roles;
@@ -209,6 +209,6 @@ export const prepareMyTeamRecord = ({action, database, value}: DataFactoryArgs)
database,
tableName: MY_TEAM,
value,
generator,
fieldsMapper,
});
};

View File

@@ -1,24 +1,24 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
prepareChannelMembershipRecord,
preparePreferenceRecord,
prepareReactionRecord,
prepareUserRecord,
} from '@database/operator/prepareRecords/user';
transformChannelMembershipRecord,
transformPreferenceRecord,
transformReactionRecord,
transformUserRecord,
} from '@database/operator/server_data_operator/transformers/user';
// See LICENSE.txt for license information.
import {createTestConnection} from '@database/operator/utils/create_test_connection';
import {OperationType} from '@typings/database/enums';
describe('*** USER Prepare Records Test ***', () => {
it('=> prepareChannelMembershipRecord: should return an array of type ChannelMembership', async () => {
it('=> transformChannelMembershipRecord: should return an array of type ChannelMembership', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'user_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareChannelMembershipRecord({
const preparedRecords = await transformChannelMembershipRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -50,13 +50,13 @@ describe('*** USER Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('ChannelMembership');
});
it('=> preparePreferenceRecord: should return an array of type Preference', async () => {
it('=> transformPreferenceRecord: should return an array of type Preference', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'user_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await preparePreferenceRecord({
const preparedRecords = await transformPreferenceRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -69,13 +69,13 @@ describe('*** USER Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('Preference');
});
it('=> prepareReactionRecord: should return an array of type Reaction', async () => {
it('=> transformReactionRecord: should return an array of type Reaction', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'user_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareReactionRecord({
const preparedRecords = await transformReactionRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -96,13 +96,13 @@ describe('*** USER Prepare Records Test ***', () => {
expect(preparedRecords!.collection.modelClass.name).toBe('Reaction');
});
it('=> prepareUserRecord: should return an array of type User', async () => {
it('=> transformUserRecord: should return an array of type User', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'user_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecords = await prepareUserRecord({
const preparedRecords = await transformUserRecord({
action: OperationType.CREATE,
database: database!,
value: {
@@ -143,7 +143,7 @@ describe('*** USER Prepare Records Test ***', () => {
timezone: {
automaticTimezone: 'Indian/Mauritius',
manualTimezone: '',
useAutomaticTimezone: true,
useAutomaticTimezone: 'true',
},
},
},

View File

@@ -2,13 +2,13 @@
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/prepareRecords/index';
import ChannelMembership from '@typings/database/channel_membership';
import {DataFactoryArgs, RawChannelMembership, RawPreference, RawReaction, RawUser} from '@typings/database/database';
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
import ChannelMembership from '@typings/database/models/servers/channel_membership';
import {TransformerArgs, RawChannelMembership, RawPreference, RawReaction, RawUser} from '@typings/database/database';
import {OperationType} from '@typings/database/enums';
import Preference from '@typings/database/preference';
import Reaction from '@typings/database/reaction';
import User from '@typings/database/user';
import Preference from '@typings/database/models/servers/preference';
import Reaction from '@typings/database/models/servers/reaction';
import User from '@typings/database/models/servers/user';
const {
CHANNEL_MEMBERSHIP,
@@ -18,19 +18,19 @@ const {
} = MM_TABLES.SERVER;
/**
* prepareReactionRecord: Prepares record of entity 'REACTION' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* transformReactionRecord: Prepares a record of the SERVER database 'Reaction' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareReactionRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformReactionRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawReaction;
const record = value.record as Reaction;
const isCreateAction = action === OperationType.CREATE;
// id of reaction comes from server response
const generator = (reaction: Reaction) => {
const fieldsMapper = (reaction: Reaction) => {
reaction._raw.id = isCreateAction ? (raw?.id ?? reaction.id) : record.id;
reaction.userId = raw.user_id;
reaction.postId = raw.post_id;
@@ -43,24 +43,24 @@ export const prepareReactionRecord = ({action, database, value}: DataFactoryArgs
database,
tableName: REACTION,
value,
generator,
fieldsMapper,
});
};
/**
* prepareUserRecord: Prepares record of entity 'USER' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* transformUserRecord: Prepares a record of the SERVER database 'User' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareUserRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformUserRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawUser;
const record = value.record as User;
const isCreateAction = action === OperationType.CREATE;
// id of user comes from server response
const generator = (user: User) => {
const fieldsMapper = (user: User) => {
user._raw.id = isCreateAction ? (raw?.id ?? user.id) : record.id;
user.authService = raw.auth_service;
user.deleteAt = raw.delete_at;
@@ -86,24 +86,24 @@ export const prepareUserRecord = ({action, database, value}: DataFactoryArgs) =>
database,
tableName: USER,
value,
generator,
fieldsMapper,
});
};
/**
* preparePreferenceRecord: Prepares record of entity 'PREFERENCE' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* transformPreferenceRecord: Prepares a record of the SERVER database 'Preference' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const preparePreferenceRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformPreferenceRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawPreference;
const record = value.record as Preference;
const isCreateAction = action === OperationType.CREATE;
// id of preference comes from server response
const generator = (preference: Preference) => {
const fieldsMapper = (preference: Preference) => {
preference._raw.id = isCreateAction ? preference.id : record.id;
preference.category = raw.category;
preference.name = raw.name;
@@ -116,24 +116,24 @@ export const preparePreferenceRecord = ({action, database, value}: DataFactoryAr
database,
tableName: PREFERENCE,
value,
generator,
fieldsMapper,
});
};
/**
* prepareChannelMembershipRecord: Prepares record of entity 'CHANNEL_MEMBERSHIP' from the SERVER database for update or create actions.
* @param {DataFactoryArgs} operator
* transformChannelMembershipRecord: Prepares a record of the SERVER database 'ChannelMembership' table for update or create actions.
* @param {TransformerArgs} operator
* @param {Database} operator.database
* @param {RecordPair} operator.value
* @returns {Promise<Model>}
*/
export const prepareChannelMembershipRecord = ({action, database, value}: DataFactoryArgs) => {
export const transformChannelMembershipRecord = ({action, database, value}: TransformerArgs) => {
const raw = value.raw as RawChannelMembership;
const record = value.record as ChannelMembership;
const isCreateAction = action === OperationType.CREATE;
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
const generator = (channelMember: ChannelMembership) => {
const fieldsMapper = (channelMember: ChannelMembership) => {
channelMember._raw.id = isCreateAction ? (raw?.id ?? channelMember.id) : record.id;
channelMember.channelId = raw.channel_id;
channelMember.userId = raw.user_id;
@@ -144,6 +144,6 @@ export const prepareChannelMembershipRecord = ({action, database, value}: DataFa
database,
tableName: CHANNEL_MEMBERSHIP,
value,
generator,
fieldsMapper,
});
};

View File

@@ -4,25 +4,20 @@
import DatabaseManager from '@database/manager';
import {DatabaseType} from '@typings/database/enums';
// NOTE: uncomment the below line if you are manually testing the database
jest.mock('@database/manager');
export const createTestConnection = async ({databaseName = 'db_name', setActive = false}) => {
const serverUrl = 'https://appv2.mattermost.com';
const databaseClient = new DatabaseManager();
const database = await databaseClient.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
await DatabaseManager.init([]);
const server = await DatabaseManager.createServerDatabase({
config: {
dbName: databaseName,
dbType: DatabaseType.SERVER,
serverUrl,
},
});
if (setActive) {
await databaseClient.setActiveServerDatabase(serverUrl);
if (setActive && server) {
await DatabaseManager.setActiveServerDatabase(serverUrl);
}
return database;
return server?.database;
};

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import Channel from '@typings/database/channel';
import Channel from '@typings/database/models/servers/channel';
import {
IdenticalRecordArgs,
RangeOfValueArgs,
@@ -15,15 +15,15 @@ import {
RecordPair,
RetrieveRecordsArgs,
} from '@typings/database/database';
import Post from '@typings/database/post';
import SlashCommand from '@typings/database/slash_command';
import Team from '@typings/database/team';
import User from '@typings/database/user';
import Post from '@typings/database/models/servers/post';
import SlashCommand from '@typings/database/models/servers/slash_command';
import Team from '@typings/database/models/servers/team';
import User from '@typings/database/models/servers/user';
const {CHANNEL, POST, SLASH_COMMAND, TEAM, USER} = MM_TABLES.SERVER;
/**
* getValidRecordsForUpdate: Database Operations on some entities are expensive. As such, we would like to operate if and only if we are
* getValidRecordsForUpdate: Database Operations on some tables are expensive. As such, we would like to operate if and only if we are
* 100% sure that the records are actually different from what we already have in the database.
* @param {IdenticalRecordArgs} identicalRecord
* @param {string} identicalRecord.tableName

View File

@@ -4,7 +4,7 @@ import {Q} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
import {RecordPair, SanitizeReactionsArgs} from '@typings/database/database';
import Reaction from '@typings/database/reaction';
import Reaction from '@typings/database/models/servers/reaction';
const {REACTION} = MM_TABLES.SERVER;
@@ -24,7 +24,7 @@ export const sanitizeReactions = async ({database, post_id, rawReactions}: Sanit
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
// similarObjects: Contains objects that are in both the RawReaction array and in the Reaction table
const similarObjects: Reaction[] = [];
const createReactions: RecordPair[] = [];

View File

@@ -1,18 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Operator from '@database/operator';
import DatabaseManager from '@database/manager';
import {createPostsChain, sanitizePosts} from '@database/operator/utils/post';
import {sanitizeReactions} from '@database/operator/utils/reaction';
import DatabaseManager from '@database/manager';
import {DatabaseType} from '@typings/database/enums';
import {RawPost} from '@typings/database/database';
import Reaction from '@typings/database/reaction';
import Reaction from '@typings/database/models/servers/reaction';
import {mockedPosts, mockedReactions} from './mock';
jest.mock('@database/manager');
describe('DataOperator: Utils tests', () => {
it('=> sanitizePosts: should filter between ordered and unordered posts', () => {
const {postsOrdered, postsUnordered} = sanitizePosts({
@@ -71,22 +67,15 @@ describe('DataOperator: Utils tests', () => {
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 databaseManagerClient = new DatabaseManager();
const database = await databaseManagerClient.createDatabaseConnection({
shouldAddToDefaultDatabase: true,
configs: {
actionsEnabled: true,
const server = await DatabaseManager.createServerDatabase({
config: {
dbName,
dbType: DatabaseType.SERVER,
serverUrl,
},
});
await databaseManagerClient.setActiveServerDatabase(serverUrl);
const operatorClient = new Operator(database!);
// we commit one Reaction to our database
const prepareRecords = await operatorClient.handleReactions({
const prepareRecords = await server?.operator.handleReactions({
reactions: [
{
user_id: 'beqkgo4wzbn98kjzjgc1p5n91o',
@@ -102,8 +91,8 @@ describe('DataOperator: Utils tests', () => {
// 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);
await server?.database!.action(async () => {
await server.database!.batch(...prepareRecords);
});
const {
@@ -111,7 +100,7 @@ describe('DataOperator: Utils tests', () => {
createEmojis,
deleteReactions,
} = await sanitizeReactions({
database: database!,
database: server!.database,
post_id: '8ww8kb1dbpf59fu4d5xhu5nf5w',
rawReactions: mockedReactions,
});

View File

@@ -1,20 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Operator from '@database/operator/index';
import DatabaseManager from '@database/manager';
import DatabaseConnectionException from '@database/exceptions/database_connection_exception';
export const createDataOperator = async (serverUrl: string) => {
const databaseManagerClient = new DatabaseManager();
const connection = await databaseManagerClient.getDatabaseConnection({serverUrl, setAsActiveDatabase: false});
if (connection) {
const operator = new Operator(connection);
return operator;
}
throw new DatabaseConnectionException(
`No database has been registered with this url: ${serverUrl}`,
);
};

View File

@@ -1,339 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {createDataOperator} from '@database/operator/wrapper/index';
import {DatabaseType} from '@typings/database/enums';
jest.mock('@database/manager');
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('*** DataOperator Wrapper ***', () => {
let databaseManagerClient: DatabaseManager;
beforeAll(async () => {
databaseManagerClient = new DatabaseManager();
});
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 databaseManagerClient.createDatabaseConnection({
configs: {
actionsEnabled: true,
dbName: 'community mattermost',
dbType: DatabaseType.SERVER,
serverUrl,
},
shouldAddToDefaultDatabase: true,
});
const dataOperator = await createDataOperator(serverUrl);
expect(dataOperator).toBeTruthy();
});
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 databaseManagerClient.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 spyOnHandleCustomEmojis = 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,
},
],
prepareRecordsOnly: 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=',
},
],
prepareRecordsOnly: 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',
},
],
prepareRecordsOnly: true,
});
expect(spyOnHandleCustomEmojis).toHaveBeenCalledTimes(1);
expect(spyOnHandleCustomEmojis).toHaveBeenCalledWith({
tableName: 'CustomEmoji',
prepareRecordsOnly: false,
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));
});
});

View File

@@ -4,13 +4,13 @@
import {AppSchema, appSchema, tableSchema} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
const {APP, GLOBAL, SERVERS} = MM_TABLES.DEFAULT;
const {INFO, GLOBAL, SERVERS} = MM_TABLES.APP;
export const defaultSchema: AppSchema = appSchema({
export const schema: AppSchema = appSchema({
version: 1,
tables: [
tableSchema({
name: APP,
name: INFO,
columns: [
{name: 'build_number', type: 'string'},
{name: 'created_at', type: 'number'},

View File

@@ -5,10 +5,10 @@ import {tableSchema} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
const {APP} = MM_TABLES.DEFAULT;
const {INFO} = MM_TABLES.APP;
export default tableSchema({
name: APP,
name: INFO,
columns: [
{name: 'build_number', type: 'string'},
{name: 'created_at', type: 'number'},

View File

@@ -5,7 +5,7 @@ import {tableSchema} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
const {GLOBAL} = MM_TABLES.DEFAULT;
const {GLOBAL} = MM_TABLES.APP;
export default tableSchema({
name: GLOBAL,

View File

@@ -5,7 +5,7 @@ import {tableSchema} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
const {SERVERS} = MM_TABLES.DEFAULT;
const {SERVERS} = MM_TABLES.APP;
export default tableSchema({
name: SERVERS,

View File

@@ -3,17 +3,17 @@
import {MM_TABLES} from '@constants/database';
import {defaultSchema} from './index';
import {schema} from './index';
const {APP, GLOBAL, SERVERS} = MM_TABLES.DEFAULT;
const {INFO, GLOBAL, SERVERS} = MM_TABLES.APP;
describe('*** Test schema for DEFAULT database ***', () => {
it('=> The DEFAULT SCHEMA should strictly match', () => {
expect(defaultSchema).toEqual({
expect(schema).toEqual({
version: 1,
tables: {
[APP]: {
name: APP,
[INFO]: {
name: INFO,
columns: {
build_number: {name: 'build_number', type: 'string'},
created_at: {name: 'created_at', type: 'number'},

View File

@@ -19,22 +19,19 @@ const ASYNC_STORAGE_CURRENT_SERVER_KEY = '@currentServerUrl';
// At some point we can remove this function and rely solely on
// the database manager's `getActiveServerUrl`.
export const getActiveServerUrl = async () => {
let serverUrl: string | null | undefined;
const databaseManager = new DatabaseManager();
serverUrl = await databaseManager.getActiveServerUrl(); // TODO: need funciton to get active server url
let serverUrl = await DatabaseManager.getActiveServerUrl();
if (!serverUrl) {
// If upgrading from non-Gekidou, the server URL might be in
// AsyncStorage. If so, retrieve the server URL, create a DB for it,
// then delete the AsyncStorage item.
serverUrl = await AsyncStorage.getItem(ASYNC_STORAGE_CURRENT_SERVER_KEY);
if (serverUrl) {
databaseManager.setActiveServerDatabase(serverUrl);
DatabaseManager.setActiveServerDatabase(serverUrl);
AsyncStorage.removeItem(ASYNC_STORAGE_CURRENT_SERVER_KEY);
}
}
return serverUrl;
return serverUrl || undefined;
};
export const setServerCredentials = (serverUrl: string, userId: string, token: string) => {
@@ -59,15 +56,6 @@ export const setServerCredentials = (serverUrl: string, userId: string, token: s
}
};
export const getActiveServerCredentials = async () => {
const serverUrl = await getActiveServerUrl();
if (serverUrl) {
return getServerCredentials(serverUrl);
}
return null;
};
export const removeServerCredentials = async (serverUrl: string) => {
// TODO: invalidate client and remove tokens

View File

@@ -5,7 +5,7 @@ import {Linking} from 'react-native';
import {Notifications} from 'react-native-notifications';
import {Screens} from '@constants';
import {getActiveServerCredentials, getServerCredentials} from '@init/credentials';
import {getActiveServerUrl, getServerCredentials} from '@init/credentials';
import {goToScreen, resetToChannel, resetToSelectServer} from '@screens/navigation';
import {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkType, DeepLinkWithData, LaunchProps, LaunchType} from '@typings/launch';
import {parseDeepLink} from '@utils/url';
@@ -52,11 +52,14 @@ const launchApp = async (props: LaunchProps, resetNavigation = true) => {
}
}
const credentials = serverUrl ? await getServerCredentials(serverUrl) : await getActiveServerCredentials();
serverUrl = await getActiveServerUrl();
if (serverUrl) {
const credentials = await getServerCredentials(serverUrl);
if (credentials) {
launchToChannel(props, resetNavigation);
return;
if (credentials) {
launchToChannel({...props, serverUrl}, resetNavigation);
return;
}
}
launchToServer(props, resetNavigation);

View File

@@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
import {getActiveServerDatabase} from '@utils/database';
import {AppState, AppStateStatus, DeviceEventEmitter, EmitterSubscription, Platform} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import {
@@ -17,11 +16,10 @@ import {
} from 'react-native-notifications';
import {Device, Navigation, View} from '@constants';
import Operator from '@database/operator';
import DatabaseManager from '@database/manager';
import {getLaunchPropsFromNotification, relaunchApp} from '@init/launch';
import NativeNotifications from '@notifications';
import {showOverlay} from '@screens/navigation';
import {IsolatedEntities} from '@typings/database/enums';
const CATEGORY = 'CAN_REPLY';
const REPLY_ACTION = 'REPLY_ACTION';
@@ -199,17 +197,14 @@ class PushNotifications {
prefix = Device.PUSH_NOTIFY_ANDROID_REACT_NATIVE;
}
const {activeServerDatabase, error} = await getActiveServerDatabase();
const operator = DatabaseManager.appDatabase?.operator;
if (!activeServerDatabase) {
return {error};
if (!operator) {
return {error: 'No App database found'};
}
const operator = new Operator(activeServerDatabase);
operator.handleIsolatedEntity({
tableName: IsolatedEntities.GLOBAL,
values: [{name: 'deviceToken', value: `${prefix}:${deviceToken}`}],
operator.handleGlobal({
global: [{name: 'deviceToken', value: `${prefix}:${deviceToken}`}],
prepareRecordsOnly: false,
});

14
app/queries/app/global.ts Normal file
View File

@@ -0,0 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {Database, Q} from '@nozbe/watermelondb';
import type Global from '@typings/database/models/app/global';
const {APP: {GLOBAL}} = MM_TABLES;
export const getDeviceToken = async (appDatabase: Database) => {
const tokens = (await appDatabase.collections.get(GLOBAL).query(Q.where('name', 'deviceToken')).fetch()) as Global[];
return tokens?.[0]?.value ?? '';
};

View File

@@ -0,0 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {Database, Q} from '@nozbe/watermelondb';
import type Servers from '@typings/database/models/app/servers';
const {APP: {SERVERS}} = MM_TABLES;
export const getServer = async (appDatabase: Database, serverUrl: string) => {
const servers = (await appDatabase.collections.get(SERVERS).query(Q.where('url', serverUrl)).fetch()) as Servers[];
return servers?.[0];
};
export const getAllServers = async (appDatabse: Database) => {
return (await appDatabse.collections.get(MM_TABLES.APP.SERVERS).query().fetch()) as Servers[];
};
export const getActiveServer = async (appDatabse: Database) => {
try {
const servers = await getAllServers(appDatabse);
return servers.reduce((a, b) => (b.lastActiveAt > a.lastActiveAt ? b : a));
} catch {
return undefined;
}
};

View File

@@ -1,13 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {Database, Q} from '@nozbe/watermelondb';
import Global from '@typings/database/global';
const {DEFAULT: {GLOBAL}} = MM_TABLES;
export const getDeviceToken = async (defaultDatabase: Database) => {
const tokens = (await defaultDatabase.collections.get(GLOBAL).query(Q.where('name', 'deviceToken')).fetch()) as Global[];
return tokens?.[0]?.value ?? '';
};

View File

@@ -4,7 +4,7 @@
import {Database} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
import Role from '@typings/database/role';
import Role from '@typings/database/models/servers/role';
const {SERVER: {ROLE}} = MM_TABLES;

Some files were not shown because too many files have changed in this diff Show More