diff --git a/app/constants/database.ts b/app/constants/database.ts index ec0363d175..6f2ab223ee 100644 --- a/app/constants/database.ts +++ b/app/constants/database.ts @@ -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', diff --git a/app/database/components/index.tsx b/app/database/components/index.tsx index 7acef0f734..b9bdfcfcc7 100644 --- a/app/database/components/index.tsx +++ b/app/database/components/index.tsx @@ -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( Component: ComponentType, ): ComponentType { return function ServerDatabaseComponent(props) { const [database, setDatabase] = useState(); + 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; diff --git a/app/database/manager/__mocks__/index.ts b/app/database/manager/__mocks__/index.ts index 4c8d88efda..fa07e6f010 100644 --- a/app/database/manager/__mocks__/index.ts +++ b/app/database/manager/__mocks__/index.ts @@ -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} - */ - 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 => { + 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 => { + 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} - */ - createDatabaseConnection = async ({configs, shouldAddToDefaultDatabase = true}: DatabaseConnectionArgs): Promise => { - 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 => { + 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} - */ - 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 => { + 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 => { + 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} - */ - 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 => { - const defaultDatabase = await this.getDefaultDatabase(); + private isServerPresent = async (serverUrl: string): Promise => { + 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 => { + 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} - */ - getActiveServerDatabase = async (): Promise => { - 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 => { + 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 => { - 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} - */ - retrieveDatabaseInstances = async (serverUrls: string[]): Promise => { - // Retrieve all server records from the default db - const allServers = await this.getAllServers(serverUrls); + public setActiveServerDatabase = async (serverUrl: string): Promise => { + 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 => { + 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} - */ - deleteDatabase = async (serverUrl: string): Promise => { - 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 => { + 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 => { + 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} - */ - 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} - */ - private setDefaultDatabase = async (): Promise => { - this.defaultDatabase = await this.createDatabaseConnection({ - configs: {dbName: DEFAULT_DATABASE}, - shouldAddToDefaultDatabase: false, - }); + factoryReset = async (shouldRemoveDirectory: boolean): Promise => { + 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} - */ - 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(); diff --git a/app/database/manager/index.ts b/app/database/manager/index.ts index 4295a81ad7..b6b9ad42fa 100644 --- a/app/database/manager/index.ts +++ b/app/database/manager/index.ts @@ -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} - */ - 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} + */ + public init = async (serverUrls: string[]): Promise => { + 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} + * createAppDatabase: Creates the App database. However, + * if a database could not be created, it will return undefined. + * @returns {Promise} */ - createDatabaseConnection = async ({configs, shouldAddToDefaultDatabase = true}: DatabaseConnectionArgs): Promise => { - const {actionsEnabled = true, dbName = DEFAULT_DATABASE, dbType = DatabaseType.DEFAULT, serverUrl = undefined} = configs; - + private createAppDatabase = async (): Promise => { 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} + * 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} */ - 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 => { + 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 => { - 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} - */ - getActiveServerDatabase = async (): Promise => { - 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} + */ + private initServerDatabase = async (serverUrl: string): Promise => { + await this.createServerDatabase({ + config: { + dbName: serverUrl, + dbType: DatabaseType.SERVER, + serverUrl, + }, + }); }; /** - * getDefaultDatabase : Returns the default database. - * @returns {Database} default database - */ - getDefaultDatabase = async (): Promise => { - 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} + */ + private addServerToAppDatabase = async ({databaseFilePath, displayName, serverUrl}: RegisterServerDatabaseArgs): Promise => { + 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} - */ - retrieveDatabaseInstances = async (serverUrls: string[]): Promise => { - // 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} + */ + private isServerPresent = async (serverUrl: string): Promise => { + 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} + */ + public getActiveServerUrl = async (): Promise => { + 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} + */ + public getActiveServerDatabase = async (): Promise => { + 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} + */ + public setActiveServerDatabase = async (serverUrl: string): Promise => { + 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} */ - deleteDatabase = async (serverUrl: string): Promise => { - 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 => { + 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} + */ + public destroyServerDatabase = async (serverUrl: string): Promise => { + 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} + */ + private deleteServerDatabaseFiles = async (serverUrl: string): Promise => { + 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} - */ - 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} - */ - 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} - */ - private setDefaultDatabase = async (): Promise => { - 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} - */ - 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(); diff --git a/app/database/manager/test.ts b/app/database/manager/test.ts index c3cb696f7f..06773592b2 100644 --- a/app/database/manager/test.ts +++ b/app/database/manager/test.ts @@ -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]); }); }); diff --git a/app/database/manager/test_manual.ts b/app/database/manager/test_manual.ts index 7d2a1a3354..298f8adda9 100644 --- a/app/database/manager/test_manual.ts +++ b/app/database/manager/test_manual.ts @@ -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(); diff --git a/app/database/migration/default/index.ts b/app/database/migration/app/index.ts similarity index 100% rename from app/database/migration/default/index.ts rename to app/database/migration/app/index.ts diff --git a/app/database/models/default/global.ts b/app/database/models/app/global.ts similarity index 91% rename from app/database/models/default/global.ts rename to app/database/models/app/global.ts index 43e0b00569..589ed8bb71 100644 --- a/app/database/models/default/global.ts +++ b/app/database/models/app/global.ts @@ -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' */ diff --git a/app/database/models/default/index.ts b/app/database/models/app/index.ts similarity index 83% rename from app/database/models/default/index.ts rename to app/database/models/app/index.ts index b94b2eb522..7f0c8c0fa4 100644 --- a/app/database/models/default/index.ts +++ b/app/database/models/app/index.ts @@ -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'; diff --git a/app/database/models/default/app.ts b/app/database/models/app/info.ts similarity index 84% rename from app/database/models/default/app.ts rename to app/database/models/app/info.ts index 97773363dc..1c331325bf 100644 --- a/app/database/models/default/app.ts +++ b/app/database/models/app/info.ts @@ -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; diff --git a/app/database/models/default/servers.ts b/app/database/models/app/servers.ts similarity index 94% rename from app/database/models/default/servers.ts rename to app/database/models/app/servers.ts index c98490fed4..a646618140 100644 --- a/app/database/models/default/servers.ts +++ b/app/database/models/app/servers.ts @@ -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 */ diff --git a/app/database/models/server/channel.ts b/app/database/models/server/channel.ts index f7511fc6de..4755dc933e 100644 --- a/app/database/models/server/channel.ts +++ b/app/database/models/server/channel.ts @@ -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; - /** 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; /** membership : Query returning the membership data for the current user if it belongs to this channel */ diff --git a/app/database/models/server/channel_info.ts b/app/database/models/server/channel_info.ts index 1ea1669fc6..4e7cd720e0 100644 --- a/app/database/models/server/channel_info.ts +++ b/app/database/models/server/channel_info.ts @@ -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; } diff --git a/app/database/models/server/channel_membership.ts b/app/database/models/server/channel_membership.ts index 319fcdf2f8..c645088796 100644 --- a/app/database/models/server/channel_membership.ts +++ b/app/database/models/server/channel_membership.ts @@ -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 */ diff --git a/app/database/models/server/custom_emoji.ts b/app/database/models/server/custom_emoji.ts index 596bf2f6e0..a28d32495b 100644 --- a/app/database/models/server/custom_emoji.ts +++ b/app/database/models/server/custom_emoji.ts @@ -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*/ diff --git a/app/database/models/server/draft.ts b/app/database/models/server/draft.ts index ad2f246e92..7d36d27b97 100644 --- a/app/database/models/server/draft.ts +++ b/app/database/models/server/draft.ts @@ -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[]; } diff --git a/app/database/models/server/file.ts b/app/database/models/server/file.ts index 767a3df654..f2154b3e0b 100644 --- a/app/database/models/server/file.ts +++ b/app/database/models/server/file.ts @@ -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. */ diff --git a/app/database/models/server/group.ts b/app/database/models/server/group.ts index 5a2720587a..e496d9a1b4 100644 --- a/app/database/models/server/group.ts +++ b/app/database/models/server/group.ts @@ -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 */ diff --git a/app/database/models/server/group_membership.ts b/app/database/models/server/group_membership.ts index 52e92e673f..b5a2aa72da 100644 --- a/app/database/models/server/group_membership.ts +++ b/app/database/models/server/group_membership.ts @@ -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 */ diff --git a/app/database/models/server/groups_in_channel.ts b/app/database/models/server/groups_in_channel.ts index 1843a55634..1053211cf6 100644 --- a/app/database/models/server/groups_in_channel.ts +++ b/app/database/models/server/groups_in_channel.ts @@ -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) */ diff --git a/app/database/models/server/groups_in_team.ts b/app/database/models/server/groups_in_team.ts index bced43d27d..c67640f025 100644 --- a/app/database/models/server/groups_in_team.ts +++ b/app/database/models/server/groups_in_team.ts @@ -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 */ diff --git a/app/database/models/server/my_channel.ts b/app/database/models/server/my_channel.ts index ed91ec627d..abe8c61d70 100644 --- a/app/database/models/server/my_channel.ts +++ b/app/database/models/server/my_channel.ts @@ -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; } diff --git a/app/database/models/server/my_channel_settings.ts b/app/database/models/server/my_channel_settings.ts index d76fd15d55..617e031a9a 100644 --- a/app/database/models/server/my_channel_settings.ts +++ b/app/database/models/server/my_channel_settings.ts @@ -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; } diff --git a/app/database/models/server/my_team.ts b/app/database/models/server/my_team.ts index 85177fecfe..801578e054 100644 --- a/app/database/models/server/my_team.ts +++ b/app/database/models/server/my_team.ts @@ -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; } diff --git a/app/database/models/server/post.ts b/app/database/models/server/post.ts index 6392af0adb..af5a7e0811 100644 --- a/app/database/models/server/post.ts +++ b/app/database/models/server/post.ts @@ -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) */ diff --git a/app/database/models/server/post_metadata.ts b/app/database/models/server/post_metadata.ts index 15e8014cc0..e8170ba52c 100644 --- a/app/database/models/server/post_metadata.ts +++ b/app/database/models/server/post_metadata.ts @@ -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)*/ diff --git a/app/database/models/server/posts_in_channel.ts b/app/database/models/server/posts_in_channel.ts index 35b38dce48..1fdf80bbd1 100644 --- a/app/database/models/server/posts_in_channel.ts +++ b/app/database/models/server/posts_in_channel.ts @@ -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)*/ diff --git a/app/database/models/server/posts_in_thread.ts b/app/database/models/server/posts_in_thread.ts index b3a6fdc8e3..bd4bc01b2f 100644 --- a/app/database/models/server/posts_in_thread.ts +++ b/app/database/models/server/posts_in_thread.ts @@ -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)*/ diff --git a/app/database/models/server/preference.ts b/app/database/models/server/preference.ts index baa4490250..466278c3c4 100644 --- a/app/database/models/server/preference.ts +++ b/app/database/models/server/preference.ts @@ -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)*/ diff --git a/app/database/models/server/reaction.ts b/app/database/models/server/reaction.ts index 53f114d4e1..37fcc79f6c 100644 --- a/app/database/models/server/reaction.ts +++ b/app/database/models/server/reaction.ts @@ -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) */ diff --git a/app/database/models/server/role.ts b/app/database/models/server/role.ts index b9437c982a..84f4732ef4 100644 --- a/app/database/models/server/role.ts +++ b/app/database/models/server/role.ts @@ -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 */ diff --git a/app/database/models/server/slash_command.ts b/app/database/models/server/slash_command.ts index fca5184c14..120c8b1936 100644 --- a/app/database/models/server/slash_command.ts +++ b/app/database/models/server/slash_command.ts @@ -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) */ diff --git a/app/database/models/server/system.ts b/app/database/models/server/system.ts index 9891814be2..05e9c5b4d4 100644 --- a/app/database/models/server/system.ts +++ b/app/database/models/server/system.ts @@ -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 */ diff --git a/app/database/models/server/team.ts b/app/database/models/server/team.ts index 6e35a04aed..eb385a59ef 100644 --- a/app/database/models/server/team.ts +++ b/app/database/models/server/team.ts @@ -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 */ diff --git a/app/database/models/server/team_channel_history.ts b/app/database/models/server/team_channel_history.ts index 295a676f3b..5507e00df5 100644 --- a/app/database/models/server/team_channel_history.ts +++ b/app/database/models/server/team_channel_history.ts @@ -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 */ diff --git a/app/database/models/server/team_membership.ts b/app/database/models/server/team_membership.ts index 13099c88a4..680e85379b 100644 --- a/app/database/models/server/team_membership.ts +++ b/app/database/models/server/team_membership.ts @@ -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 */ diff --git a/app/database/models/server/team_search_history.ts b/app/database/models/server/team_search_history.ts index 3a3405a9f8..69eab408b5 100644 --- a/app/database/models/server/team_search_history.ts +++ b/app/database/models/server/team_search_history.ts @@ -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 */ diff --git a/app/database/models/server/terms_of_service.ts b/app/database/models/server/terms_of_service.ts index 8bc7a3d6da..3c977da7cb 100644 --- a/app/database/models/server/terms_of_service.ts +++ b/app/database/models/server/terms_of_service.ts @@ -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 */ diff --git a/app/database/models/server/user.ts b/app/database/models/server/user.ts index c4e14b0a3f..7d5da225f0 100644 --- a/app/database/models/server/user.ts +++ b/app/database/models/server/user.ts @@ -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 */ diff --git a/app/database/operator/app_data_operator/comparator/index.ts b/app/database/operator/app_data_operator/comparator/index.ts new file mode 100644 index 0000000000..22932639e0 --- /dev/null +++ b/app/database/operator/app_data_operator/comparator/index.ts @@ -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; +}; diff --git a/app/database/operator/app_data_operator/index.test.ts b/app/database/operator/app_data_operator/index.test.ts new file mode 100644 index 0000000000..d485428e39 --- /dev/null +++ b/app/database/operator/app_data_operator/index.test.ts @@ -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, + }); + }); +}); diff --git a/app/database/operator/app_data_operator/index.ts b/app/database/operator/app_data_operator/index.ts new file mode 100644 index 0000000000..151a5fd429 --- /dev/null +++ b/app/database/operator/app_data_operator/index.ts @@ -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; + } +} diff --git a/app/database/operator/app_data_operator/transformers/index.ts b/app/database/operator/app_data_operator/transformers/index.ts new file mode 100644 index 0000000000..307f99456f --- /dev/null +++ b/app/database/operator/app_data_operator/transformers/index.ts @@ -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} + */ +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} + */ +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} + */ +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, + }); +}; diff --git a/app/database/operator/app_data_operator/transformers/test.ts b/app/database/operator/app_data_operator/transformers/test.ts new file mode 100644 index 0000000000..5afa8a5432 --- /dev/null +++ b/app/database/operator/app_data_operator/transformers/test.ts @@ -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'); + }); +}); diff --git a/app/database/operator/base_data_operator/index.ts b/app/database/operator/base_data_operator/index.ts new file mode 100644 index 0000000000..1fc811b371 --- /dev/null +++ b/app/database/operator/base_data_operator/index.ts @@ -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; + processRecords: ({createOrUpdateRawValues, deleteRawValues, tableName, findMatchingRecordBy, fieldName}: ProcessRecordsArgs) => Promise; + batchRecords: (models: Model[]) => Promise; + prepareRecords: ({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs) => Promise; +} + +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 => { + 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;} prepareRecord.composer + * @throws {DataOperatorException} + * @returns {Promise} + */ + prepareRecords = async ({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs) => { + if (!this.database) { + throw new DataOperatorException('Database not defined'); + } + + let preparedRecords: Promise[] = []; + + // 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} + */ + 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} handleRecordsArgs.composer + * @param {RawValue[]} handleRecordsArgs.createOrUpdateRawValues + * @param {RawValue[]} handleRecordsArgs.deleteRawValues + * @param {string} handleRecordsArgs.tableName + * @returns {Promise} + */ + 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; + }; +} diff --git a/app/database/operator/handlers/base_handler.test.ts b/app/database/operator/handlers/base_handler.test.ts deleted file mode 100644 index 8f2c633a59..0000000000 --- a/app/database/operator/handlers/base_handler.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/app/database/operator/handlers/base_handler.ts b/app/database/operator/handlers/base_handler.ts deleted file mode 100644 index 66c8215eff..0000000000 --- a/app/database/operator/handlers/base_handler.ts +++ /dev/null @@ -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; - processInputs: ({rawValues, tableName, findMatchingRecordBy, fieldName}: ProcessInputsArgs) => Promise<{ createRaws: RecordPair[]; updateRaws: RecordPair[] }>; - batchOperations: ({database, models}: BatchOperationsArgs) => Promise; - prepareRecords: ({database, tableName, createRaws, updateRaws, recordOperator}: PrepareRecordsArgs) => Promise; - executeInDatabase: ({createRaws, recordOperator, tableName, updateRaws}: PrepareForDatabaseArgs) => Promise; - getDatabase: (tableName: string) => Database; - getDefaultDatabase: () => Promise; - getServerDatabase: () => Promise; -} - -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} handleEntityArgs.operator - * @param {RawValue[]} handleEntityArgs.rawValues - * @param {string} handleEntityArgs.tableName - * @returns {Promise} - */ - 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} - */ - 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;} prepareRecord.recordOperator - * @throws {DataOperatorException} - * @returns {Promise} - */ - 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[] = []; - - // 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} executeInDatabase.recordOperator - * @returns {Promise} - */ - 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} - */ - 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} - */ - 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} - */ - 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; diff --git a/app/database/operator/handlers/channel.test.ts b/app/database/operator/handlers/channel.test.ts deleted file mode 100644 index 35618b7ce7..0000000000 --- a/app/database/operator/handlers/channel.test.ts +++ /dev/null @@ -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, - }); - }); -}); diff --git a/app/database/operator/handlers/group.test.ts b/app/database/operator/handlers/group.test.ts deleted file mode 100644 index 2097755096..0000000000 --- a/app/database/operator/handlers/group.test.ts +++ /dev/null @@ -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, - }); - }); -}); diff --git a/app/database/operator/handlers/team.test.ts b/app/database/operator/handlers/team.test.ts deleted file mode 100644 index 5e608f63cd..0000000000 --- a/app/database/operator/handlers/team.test.ts +++ /dev/null @@ -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, - }); - }); -}); diff --git a/app/database/operator/handlers/user.test.ts b/app/database/operator/handlers/user.test.ts deleted file mode 100644 index 85e7627223..0000000000 --- a/app/database/operator/handlers/user.test.ts +++ /dev/null @@ -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, - }); - }); -}); diff --git a/app/database/operator/index.ts b/app/database/operator/index.ts deleted file mode 100644 index 54bf8ca9f9..0000000000 --- a/app/database/operator/index.ts +++ /dev/null @@ -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; diff --git a/app/database/operator/prepareRecords/general.test.ts b/app/database/operator/prepareRecords/general.test.ts deleted file mode 100644 index a13709ec2b..0000000000 --- a/app/database/operator/prepareRecords/general.test.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/app/database/operator/prepareRecords/general.ts b/app/database/operator/prepareRecords/general.ts deleted file mode 100644 index 602403b613..0000000000 --- a/app/database/operator/prepareRecords/general.ts +++ /dev/null @@ -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} - */ -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} - */ -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} - */ -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} - */ -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} - */ -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} - */ -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} - */ -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, - }); -}; diff --git a/app/database/operator/comparators/index.ts b/app/database/operator/server_data_operator/comparators/index.ts similarity index 69% rename from app/database/operator/comparators/index.ts rename to app/database/operator/server_data_operator/comparators/index.ts index 06247c11b9..1484ce7bcb 100644 --- a/app/database/operator/comparators/index.ts +++ b/app/database/operator/server_data_operator/comparators/index.ts @@ -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; }; diff --git a/app/database/operator/server_data_operator/handlers/channel.test.ts b/app/database/operator/server_data_operator/handlers/channel.test.ts new file mode 100644 index 0000000000..25a9db7983 --- /dev/null +++ b/app/database/operator/server_data_operator/handlers/channel.test.ts @@ -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, + }); + }); +}); diff --git a/app/database/operator/handlers/channel.ts b/app/database/operator/server_data_operator/handlers/channel.ts similarity index 74% rename from app/database/operator/handlers/channel.ts rename to app/database/operator/server_data_operator/handlers/channel.ts index 7d5a622158..6d8e77d71e 100644 --- a/app/database/operator/handlers/channel.ts +++ b/app/database/operator/server_data_operator/handlers/channel.ts @@ -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, }); diff --git a/app/database/operator/server_data_operator/handlers/group.test.ts b/app/database/operator/server_data_operator/handlers/group.test.ts new file mode 100644 index 0000000000..b2de70a88e --- /dev/null +++ b/app/database/operator/server_data_operator/handlers/group.test.ts @@ -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, + }); + }); +}); diff --git a/app/database/operator/handlers/group.ts b/app/database/operator/server_data_operator/handlers/group.ts similarity index 73% rename from app/database/operator/handlers/group.ts rename to app/database/operator/server_data_operator/handlers/group.ts index c935d14d2f..009031163b 100644 --- a/app/database/operator/handlers/group.ts +++ b/app/database/operator/server_data_operator/handlers/group.ts @@ -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, }); diff --git a/app/database/operator/server_data_operator/handlers/index.test.ts b/app/database/operator/server_data_operator/handlers/index.test.ts new file mode 100644 index 0000000000..fda913fab5 --- /dev/null +++ b/app/database/operator/server_data_operator/handlers/index.test.ts @@ -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); + }); +}); diff --git a/app/database/operator/server_data_operator/handlers/index.ts b/app/database/operator/server_data_operator/handlers/index.ts new file mode 100644 index 0000000000..ea0400d076 --- /dev/null +++ b/app/database/operator/server_data_operator/handlers/index.ts @@ -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} execute.recordOperator + * @returns {Promise} + */ + execute = async ({createRaws, transformer, tableName, updateRaws}: OperationArgs) => { + const models = await this.prepareRecords({ + tableName, + createRaws, + updateRaws, + transformer, + }); + + if (models?.length > 0) { + await this.batchRecords(models); + } + }; +} diff --git a/app/database/operator/handlers/post.test.ts b/app/database/operator/server_data_operator/handlers/post.test.ts similarity index 86% rename from app/database/operator/handlers/post.test.ts rename to app/database/operator/server_data_operator/handlers/post.test.ts index a9f8baf9a4..42218eaf03 100644 --- a/app/database/operator/handlers/post.test.ts +++ b/app/database/operator/server_data_operator/handlers/post.test.ts @@ -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, diff --git a/app/database/operator/handlers/post.ts b/app/database/operator/server_data_operator/handlers/post.ts similarity index 83% rename from app/database/operator/handlers/post.ts rename to app/database/operator/server_data_operator/handlers/post.ts index d4e40b4044..a78111f669 100644 --- a/app/database/operator/handlers/post.ts +++ b/app/database/operator/server_data_operator/handlers/post.ts @@ -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, 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} */ @@ -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} */ @@ -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; }); diff --git a/app/database/operator/server_data_operator/handlers/team.test.ts b/app/database/operator/server_data_operator/handlers/team.test.ts new file mode 100644 index 0000000000..4f500eedc8 --- /dev/null +++ b/app/database/operator/server_data_operator/handlers/team.test.ts @@ -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, + }); + }); +}); diff --git a/app/database/operator/handlers/team.ts b/app/database/operator/server_data_operator/handlers/team.ts similarity index 72% rename from app/database/operator/handlers/team.ts rename to app/database/operator/server_data_operator/handlers/team.ts index 457933df28..650cb3bbd9 100644 --- a/app/database/operator/handlers/team.ts +++ b/app/database/operator/server_data_operator/handlers/team.ts @@ -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, }); diff --git a/app/database/operator/server_data_operator/handlers/user.test.ts b/app/database/operator/server_data_operator/handlers/user.test.ts new file mode 100644 index 0000000000..8ac3491e4a --- /dev/null +++ b/app/database/operator/server_data_operator/handlers/user.test.ts @@ -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, + }); + }); +}); diff --git a/app/database/operator/handlers/user.ts b/app/database/operator/server_data_operator/handlers/user.ts similarity index 71% rename from app/database/operator/handlers/user.ts rename to app/database/operator/server_data_operator/handlers/user.ts index ea7cc4e094..1418aeca7c 100644 --- a/app/database/operator/handlers/user.ts +++ b/app/database/operator/server_data_operator/handlers/user.ts @@ -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; + handlePreferences : ({preferences, prepareRecordsOnly}: HandlePreferencesArgs) => Promise; + handleReactions : ({reactions, prepareRecordsOnly}: HandleReactionsArgs) => Promise<(Reaction | CustomEmoji)[]>; + handleUsers : ({users, prepareRecordsOnly}: HandleUsersArgs) => Promise; } 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} */ 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} */ 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} */ 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, }); diff --git a/app/database/operator/server_data_operator/index.ts b/app/database/operator/server_data_operator/index.ts new file mode 100644 index 0000000000..2e8b517d90 --- /dev/null +++ b/app/database/operator/server_data_operator/index.ts @@ -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; diff --git a/app/database/operator/prepareRecords/channel.test.ts b/app/database/operator/server_data_operator/transformers/channel.test.ts similarity index 82% rename from app/database/operator/prepareRecords/channel.test.ts rename to app/database/operator/server_data_operator/transformers/channel.test.ts index b26c460c32..fcb2a8bd45 100644 --- a/app/database/operator/prepareRecords/channel.test.ts +++ b/app/database/operator/server_data_operator/transformers/channel.test.ts @@ -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: { diff --git a/app/database/operator/prepareRecords/channel.ts b/app/database/operator/server_data_operator/transformers/channel.ts similarity index 68% rename from app/database/operator/prepareRecords/channel.ts rename to app/database/operator/server_data_operator/transformers/channel.ts index f934390c40..c69996d002 100644 --- a/app/database/operator/prepareRecords/channel.ts +++ b/app/database/operator/server_data_operator/transformers/channel.ts @@ -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} */ -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} */ -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} */ -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} */ -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, }); }; diff --git a/app/database/operator/server_data_operator/transformers/general.test.ts b/app/database/operator/server_data_operator/transformers/general.test.ts new file mode 100644 index 0000000000..e07873c44a --- /dev/null +++ b/app/database/operator/server_data_operator/transformers/general.test.ts @@ -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'); + }); +}); + diff --git a/app/database/operator/server_data_operator/transformers/general.ts b/app/database/operator/server_data_operator/transformers/general.ts new file mode 100644 index 0000000000..5ff51188e7 --- /dev/null +++ b/app/database/operator/server_data_operator/transformers/general.ts @@ -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} + */ +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} + */ +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} + */ +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} + */ +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, + }); +}; diff --git a/app/database/operator/prepareRecords/group.test.ts b/app/database/operator/server_data_operator/transformers/group.test.ts similarity index 80% rename from app/database/operator/prepareRecords/group.test.ts rename to app/database/operator/server_data_operator/transformers/group.test.ts index a7b820fb0a..b39d4dc64a 100644 --- a/app/database/operator/prepareRecords/group.test.ts +++ b/app/database/operator/server_data_operator/transformers/group.test.ts @@ -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: { diff --git a/app/database/operator/prepareRecords/group.ts b/app/database/operator/server_data_operator/transformers/group.ts similarity index 66% rename from app/database/operator/prepareRecords/group.ts rename to app/database/operator/server_data_operator/transformers/group.ts index 5b6cdc5e2e..9e12a48984 100644 --- a/app/database/operator/prepareRecords/group.ts +++ b/app/database/operator/server_data_operator/transformers/group.ts @@ -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} */ -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} */ -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} */ -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} */ -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, }); }; diff --git a/app/database/operator/prepareRecords/index.ts b/app/database/operator/server_data_operator/transformers/index.ts similarity index 67% rename from app/database/operator/prepareRecords/index.ts rename to app/database/operator/server_data_operator/transformers/index.ts index 90416c0d5f..ebb86780b2 100644 --- a/app/database/operator/prepareRecords/index.ts +++ b/app/database/operator/server_data_operator/transformers/index.ts @@ -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} */ export const prepareBaseRecord = async ({ @@ -21,12 +21,12 @@ export const prepareBaseRecord = async ({ database, tableName, value, - generator, -}: DataFactoryArgs): Promise => { + fieldsMapper, +}: TransformerArgs): Promise => { 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); }; diff --git a/app/database/operator/prepareRecords/post.test.ts b/app/database/operator/server_data_operator/transformers/post.test.ts similarity index 81% rename from app/database/operator/prepareRecords/post.test.ts rename to app/database/operator/server_data_operator/transformers/post.test.ts index d622cbe52b..a2b7c7e846 100644 --- a/app/database/operator/prepareRecords/post.test.ts +++ b/app/database/operator/server_data_operator/transformers/post.test.ts @@ -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: { diff --git a/app/database/operator/prepareRecords/post.ts b/app/database/operator/server_data_operator/transformers/post.ts similarity index 67% rename from app/database/operator/prepareRecords/post.ts rename to app/database/operator/server_data_operator/transformers/post.ts index d6aea7af57..7ba03d2ce0 100644 --- a/app/database/operator/prepareRecords/post.ts +++ b/app/database/operator/server_data_operator/transformers/post.ts @@ -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} */ -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} */ -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} */ -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} */ -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} */ -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} */ -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, }); }; diff --git a/app/database/operator/prepareRecords/team.test.ts b/app/database/operator/server_data_operator/transformers/team.test.ts similarity index 81% rename from app/database/operator/prepareRecords/team.test.ts rename to app/database/operator/server_data_operator/transformers/team.test.ts index 7f51d30891..838b2bfaa1 100644 --- a/app/database/operator/prepareRecords/team.test.ts +++ b/app/database/operator/server_data_operator/transformers/team.test.ts @@ -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: { diff --git a/app/database/operator/prepareRecords/team.ts b/app/database/operator/server_data_operator/transformers/team.ts similarity index 69% rename from app/database/operator/prepareRecords/team.ts rename to app/database/operator/server_data_operator/transformers/team.ts index 05c8d4df72..9f24dad356 100644 --- a/app/database/operator/prepareRecords/team.ts +++ b/app/database/operator/server_data_operator/transformers/team.ts @@ -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} */ -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} */ -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} */ -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} */ -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} */ -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} */ -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, }); }; diff --git a/app/database/operator/prepareRecords/user.test.ts b/app/database/operator/server_data_operator/transformers/user.test.ts similarity index 85% rename from app/database/operator/prepareRecords/user.test.ts rename to app/database/operator/server_data_operator/transformers/user.test.ts index 629ac50ad9..690e8c58de 100644 --- a/app/database/operator/prepareRecords/user.test.ts +++ b/app/database/operator/server_data_operator/transformers/user.test.ts @@ -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', }, }, }, diff --git a/app/database/operator/prepareRecords/user.ts b/app/database/operator/server_data_operator/transformers/user.ts similarity index 67% rename from app/database/operator/prepareRecords/user.ts rename to app/database/operator/server_data_operator/transformers/user.ts index 4c107e4721..a7c85b8bd5 100644 --- a/app/database/operator/prepareRecords/user.ts +++ b/app/database/operator/server_data_operator/transformers/user.ts @@ -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} */ -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} */ -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} */ -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} */ -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, }); }; diff --git a/app/database/operator/utils/create_test_connection.ts b/app/database/operator/utils/create_test_connection.ts index 0cc6e0bea8..0025ad0a9a 100644 --- a/app/database/operator/utils/create_test_connection.ts +++ b/app/database/operator/utils/create_test_connection.ts @@ -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; }; diff --git a/app/database/operator/utils/general.ts b/app/database/operator/utils/general.ts index 8d530d0241..eff819093c 100644 --- a/app/database/operator/utils/general.ts +++ b/app/database/operator/utils/general.ts @@ -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 diff --git a/app/database/operator/utils/reaction.ts b/app/database/operator/utils/reaction.ts index 19645bf15a..92484e6158 100644 --- a/app/database/operator/utils/reaction.ts +++ b/app/database/operator/utils/reaction.ts @@ -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[] = []; diff --git a/app/database/operator/utils/test.ts b/app/database/operator/utils/test.ts index e07e67f533..cfea868a31 100644 --- a/app/database/operator/utils/test.ts +++ b/app/database/operator/utils/test.ts @@ -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, }); diff --git a/app/database/operator/wrapper/index.ts b/app/database/operator/wrapper/index.ts deleted file mode 100644 index 4e01856234..0000000000 --- a/app/database/operator/wrapper/index.ts +++ /dev/null @@ -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}`, - ); -}; diff --git a/app/database/operator/wrapper/test.ts b/app/database/operator/wrapper/test.ts deleted file mode 100644 index 82db35dfce..0000000000 --- a/app/database/operator/wrapper/test.ts +++ /dev/null @@ -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)); - }); -}); diff --git a/app/database/schema/default/index.ts b/app/database/schema/app/index.ts similarity index 90% rename from app/database/schema/default/index.ts rename to app/database/schema/app/index.ts index a37416ae46..4530d34da4 100644 --- a/app/database/schema/default/index.ts +++ b/app/database/schema/app/index.ts @@ -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'}, diff --git a/app/database/schema/default/table_schemas/app.ts b/app/database/schema/app/table_schemas/app.ts similarity index 89% rename from app/database/schema/default/table_schemas/app.ts rename to app/database/schema/app/table_schemas/app.ts index 72532bc1db..26170e4843 100644 --- a/app/database/schema/default/table_schemas/app.ts +++ b/app/database/schema/app/table_schemas/app.ts @@ -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'}, diff --git a/app/database/schema/default/table_schemas/global.ts b/app/database/schema/app/table_schemas/global.ts similarity index 91% rename from app/database/schema/default/table_schemas/global.ts rename to app/database/schema/app/table_schemas/global.ts index a9a622574b..48f004a924 100644 --- a/app/database/schema/default/table_schemas/global.ts +++ b/app/database/schema/app/table_schemas/global.ts @@ -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, diff --git a/app/database/schema/default/table_schemas/index.ts b/app/database/schema/app/table_schemas/index.ts similarity index 100% rename from app/database/schema/default/table_schemas/index.ts rename to app/database/schema/app/table_schemas/index.ts diff --git a/app/database/schema/default/table_schemas/servers.ts b/app/database/schema/app/table_schemas/servers.ts similarity index 94% rename from app/database/schema/default/table_schemas/servers.ts rename to app/database/schema/app/table_schemas/servers.ts index ba6dc379e5..92c26a870a 100644 --- a/app/database/schema/default/table_schemas/servers.ts +++ b/app/database/schema/app/table_schemas/servers.ts @@ -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, diff --git a/app/database/schema/default/test.ts b/app/database/schema/app/test.ts similarity index 93% rename from app/database/schema/default/test.ts rename to app/database/schema/app/test.ts index ec9d8356aa..e68506156c 100644 --- a/app/database/schema/default/test.ts +++ b/app/database/schema/app/test.ts @@ -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'}, diff --git a/app/init/credentials.ts b/app/init/credentials.ts index 4b44cb281b..4f889e49a8 100644 --- a/app/init/credentials.ts +++ b/app/init/credentials.ts @@ -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 diff --git a/app/init/launch.ts b/app/init/launch.ts index 1310501aa4..f296ba5ff6 100644 --- a/app/init/launch.ts +++ b/app/init/launch.ts @@ -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); diff --git a/app/init/push_notifications.ts b/app/init/push_notifications.ts index 782ff44ca7..7d1d09f6ab 100644 --- a/app/init/push_notifications.ts +++ b/app/init/push_notifications.ts @@ -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, }); diff --git a/app/queries/app/global.ts b/app/queries/app/global.ts new file mode 100644 index 0000000000..c940772393 --- /dev/null +++ b/app/queries/app/global.ts @@ -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 ?? ''; +}; diff --git a/app/queries/app/servers.ts b/app/queries/app/servers.ts new file mode 100644 index 0000000000..e2a6b3f7d7 --- /dev/null +++ b/app/queries/app/servers.ts @@ -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; + } +}; diff --git a/app/queries/global.ts b/app/queries/global.ts deleted file mode 100644 index 981d7ece4f..0000000000 --- a/app/queries/global.ts +++ /dev/null @@ -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 ?? ''; -}; diff --git a/app/queries/role.ts b/app/queries/servers/role.ts similarity index 87% rename from app/queries/role.ts rename to app/queries/servers/role.ts index ec89b0f5b3..dc2a28673e 100644 --- a/app/queries/role.ts +++ b/app/queries/servers/role.ts @@ -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; diff --git a/app/queries/system.ts b/app/queries/servers/system.ts similarity index 95% rename from app/queries/system.ts rename to app/queries/servers/system.ts index f28da57e5d..51d933ddb7 100644 --- a/app/queries/system.ts +++ b/app/queries/servers/system.ts @@ -4,7 +4,7 @@ import {Database, Q} from '@nozbe/watermelondb'; import {MM_TABLES} from '@constants/database'; -import System from '@typings/database/system'; +import System from '@typings/database/models/servers/system'; const {SERVER: {SYSTEM}} = MM_TABLES; diff --git a/app/queries/user.ts b/app/queries/servers/user.ts similarity index 88% rename from app/queries/user.ts rename to app/queries/servers/user.ts index 6612cbcb6d..dbfc9cc7a4 100644 --- a/app/queries/user.ts +++ b/app/queries/servers/user.ts @@ -4,7 +4,7 @@ import {Database, Q} from '@nozbe/watermelondb'; import {MM_TABLES} from '@constants/database'; -import User from '@typings/database/user'; +import User from '@typings/database/models/servers/user'; export const getUserById = async ({userId, database}: { userId: string, database: Database}) => { const userRecords = (await database.collections.get(MM_TABLES.SERVER.USER).query(Q.where('id', userId)).fetch()) as User[]; diff --git a/app/requests/local/systems.ts b/app/requests/local/systems.ts index 1a5fda5b1d..ac94e2841d 100644 --- a/app/requests/local/systems.ts +++ b/app/requests/local/systems.ts @@ -1,66 +1,17 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import Operator from '@database/operator'; -import {ServerUrlChangedArgs} from '@typings/database/database'; -import {IsolatedEntities} from '@typings/database/enums'; -import System from '@typings/database/system'; -import {getActiveServerDatabase} from '@utils/database'; +import DatabaseManager from '@database/manager'; -/** - * setLastUpgradeCheck: Takes in 'config' record from System entity and update its lastUpdateCheck to Date.now() - * @param {System} configRecord - * @returns {Promise} - */ -export const setLastUpgradeCheck = async (configRecord: System) => { - const {activeServerDatabase: database, error} = await getActiveServerDatabase(); +export const createSessions = async (serverUrl: string, sessions: any) => { + const database = DatabaseManager.serverDatabases[serverUrl].database; + const operator = DatabaseManager.serverDatabases[serverUrl].operator; if (!database) { - return {error}; + return {error: `${serverUrl} database not found`}; } - await database.action(async () => { - await configRecord.update((config) => { - config.value = {...configRecord.value, lastUpdateCheck: Date.now()}; - }); - }); - - return null; -}; - -export const handleServerUrlChanged = async ({configRecord, licenseRecord, selectServerRecord, serverUrl}: ServerUrlChangedArgs) => { - const {activeServerDatabase: database, error} = await getActiveServerDatabase(); - if (!database) { - return {error}; - } - - await database.action(async () => { - await database.batch( - ...[ - configRecord.prepareUpdate((config: System) => { - config.value = {}; - }), - licenseRecord.prepareUpdate((license: System) => { - license.value = {}; - }), - selectServerRecord.prepareUpdate((server: System) => { - server.value = {...server.value, serverUrl}; - }), - ], - ); - }); - - return null; -}; - -export const createSessions = async (sessions: any) => { - const {activeServerDatabase: database, error} = await getActiveServerDatabase(); - if (!database) { - return {error}; - } - const operator = new Operator(database); - await operator.handleIsolatedEntity({ - tableName: IsolatedEntities.SYSTEM, - values: [{ + await operator.handleSystem({ + systems: [{ // id: string; // todo: to confirm value for session id ? name: 'sessions', @@ -68,17 +19,6 @@ export const createSessions = async (sessions: any) => { }], prepareRecordsOnly: false, }); + return null; }; - -export const setDeepLinkUrl = async (url: string) => { - const operator = new Operator(); - await operator.handleIsolatedEntity({ - tableName: IsolatedEntities.GLOBAL, - values: [{ - name: 'deepLinkUrl', - value: url, - }], - prepareRecordsOnly: false, - }); -}; diff --git a/app/requests/local/timezone.ts b/app/requests/local/timezone.ts index ea4a17244d..6ae33b28f8 100644 --- a/app/requests/local/timezone.ts +++ b/app/requests/local/timezone.ts @@ -3,11 +3,11 @@ import {getTimeZone} from 'react-native-localize'; -import {getUserById} from '@queries/user'; +import DatabaseManager from '@database/manager'; +import {getUserById} from '@queries/servers/user'; import {updateMe} from '@requests/remote/user'; -import {Config} from '@typings/database/config'; -import User from '@typings/database/user'; -import {getActiveServerDatabase} from '@utils/database'; +import {Config} from '@typings/database/models/servers/config'; +import User from '@typings/database/models/servers/user'; export const isTimezoneEnabled = (config: Partial) => { return config?.ExperimentalTimezone === 'true'; @@ -17,13 +17,13 @@ export function getDeviceTimezone() { return getTimeZone(); } -export const autoUpdateTimezone = async ({deviceTimezone, userId}: {deviceTimezone: string, userId: string}) => { - const {activeServerDatabase, error} = await getActiveServerDatabase(); - if (!activeServerDatabase) { - return {error}; +export const autoUpdateTimezone = async (serverUrl: string, {deviceTimezone, userId}: {deviceTimezone: string, userId: string}) => { + const database = DatabaseManager.serverDatabases[serverUrl].database; + if (!database) { + return {error: `No database present for ${serverUrl}`}; } - const currentUser = await getUserById({userId, database: activeServerDatabase}) ?? null; + const currentUser = await getUserById({userId, database}) ?? null; if (!currentUser) { return null; @@ -35,7 +35,7 @@ export const autoUpdateTimezone = async ({deviceTimezone, userId}: {deviceTimezo if (currentTimezone.useAutomaticTimezone && newTimezoneExists) { const timezone = {useAutomaticTimezone: 'true', automaticTimezone: deviceTimezone, manualTimezone: currentTimezone.manualTimezone}; const updatedUser = {...currentUser, timezone} as User; - await updateMe(updatedUser); + await updateMe(serverUrl, updatedUser); } return null; }; diff --git a/app/requests/remote/push_notification.ts b/app/requests/remote/push_notification.ts index ed408d4fb9..fda5a25258 100644 --- a/app/requests/remote/push_notification.ts +++ b/app/requests/remote/push_notification.ts @@ -5,11 +5,11 @@ import moment from 'moment-timezone'; import {IntlShape} from 'react-intl'; import {Client4} from '@client/rest'; +import DatabaseManager from '@database/manager'; import PushNotifications from '@init/push_notifications'; -import {getCommonSystemValues} from '@queries/system'; +import {getCommonSystemValues} from '@app/queries/servers/system'; import {getSessions} from '@requests/remote/user'; -import {Config} from '@typings/database/config'; -import {getActiveServerDatabase} from '@utils/database'; +import {Config} from '@typings/database/models/servers/config'; import {isMinimumServerVersion} from '@utils/helpers'; const MAJOR_VERSION = 5; @@ -23,12 +23,8 @@ const sortByNewest = (a: any, b: any) => { return 1; }; -export const scheduleExpiredNotification = async (intl: IntlShape) => { - const {activeServerDatabase: database, error} = await getActiveServerDatabase(); - if (!database) { - return {error}; - } - +export const scheduleExpiredNotification = async (serverUrl: string, intl: IntlShape) => { + const database = DatabaseManager.serverDatabases[serverUrl].database; const {currentUserId, config}: {currentUserId: string, config: Partial} = await getCommonSystemValues(database); if (isMinimumServerVersion(Client4.serverVersion, MAJOR_VERSION, MINOR_VERSION) && config.ExtendSessionLengthWithActivity === 'true') { @@ -41,7 +37,7 @@ export const scheduleExpiredNotification = async (intl: IntlShape) => { let sessions: any; try { - sessions = await getSessions(currentUserId); + sessions = await getSessions(serverUrl, currentUserId); } catch (e) { // console.warn('Failed to get current session', e); return; diff --git a/app/requests/remote/role.ts b/app/requests/remote/role.ts index 47fe724603..b2f48e9cc8 100644 --- a/app/requests/remote/role.ts +++ b/app/requests/remote/role.ts @@ -2,17 +2,12 @@ // See LICENSE.txt for license information. import {Client4} from '@client/rest'; -import Operator from '@database/operator'; -import {getRoles} from '@queries/role'; -import {IsolatedEntities} from '@typings/database/enums'; -import {getActiveServerDatabase} from '@utils/database'; - -export const loadRolesIfNeeded = async (updatedRoles: string[]) => { - const {activeServerDatabase: database, error: e} = await getActiveServerDatabase(); - if (!database) { - return {error: e}; - } +import DatabaseManager from '@database/manager'; +import {getRoles} from '@app/queries/servers/role'; +export const loadRolesIfNeeded = async (serverUrl: string, updatedRoles: string[]) => { + const database = DatabaseManager.serverDatabases[serverUrl].database; + const operator = DatabaseManager.serverDatabases[serverUrl].operator; const existingRoles = ((await getRoles(database)) as unknown) as Role[]; const roleNames = existingRoles.map((role) => { @@ -24,11 +19,10 @@ export const loadRolesIfNeeded = async (updatedRoles: string[]) => { }); try { - const operator = new Operator(database); - const data = await Client4.getRolesByNames(newRoles); - await operator.handleIsolatedEntity({ - tableName: IsolatedEntities.ROLE, - values: data, + const roles = await Client4.getRolesByNames(newRoles); + + await operator.handleRole({ + roles, prepareRecordsOnly: false, }); } catch (error) { diff --git a/app/requests/remote/systems.ts b/app/requests/remote/systems.ts index 11ee34b2f9..08d0868999 100644 --- a/app/requests/remote/systems.ts +++ b/app/requests/remote/systems.ts @@ -5,12 +5,12 @@ import {Client4} from '@client/rest'; import {logError} from '@requests/remote/error'; import {forceLogoutIfNecessary} from '@requests/remote/user'; -export const getDataRetentionPolicy = async () => { +export const getDataRetentionPolicy = async (serverUrl: string) => { let data = {}; try { data = await Client4.getDataRetentionPolicy(); } catch (error) { - forceLogoutIfNecessary(error); + forceLogoutIfNecessary(serverUrl, error); //fixme: do we care for the below line ? It seems that the `error` object is never read ?? // dispatch(batchActions([{type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, error,},])); diff --git a/app/requests/remote/user.ts b/app/requests/remote/user.ts index 40c6c9ffcb..bc00710b13 100644 --- a/app/requests/remote/user.ts +++ b/app/requests/remote/user.ts @@ -2,18 +2,18 @@ // See LICENSE.txt for license information. import {Client4} from '@client/rest'; -import Operator from '@database/operator'; +import DatabaseManager from '@database/manager'; import analytics from '@init/analytics'; import {setServerCredentials} from '@init/credentials'; -import {getDeviceToken} from '@queries/global'; -import {getCommonSystemValues, getCurrentUserId} from '@queries/system'; +import {getDeviceToken} from '@app/queries/app/global'; +import {getCommonSystemValues, getCurrentUserId} from '@app/queries/servers/system'; import {createSessions} from '@requests/local/systems'; import {autoUpdateTimezone, getDeviceTimezone, isTimezoneEnabled} from '@requests/local/timezone'; import {logError} from '@requests/remote/error'; import {loadRolesIfNeeded} from '@requests/remote/role'; import {getDataRetentionPolicy} from '@requests/remote/systems'; import {Client4Error} from '@typings/api/client4'; -import {Config} from '@typings/database/config'; +import {Config} from '@typings/database/models/servers/config'; import { LoadMeArgs, LoginArgs, @@ -24,16 +24,17 @@ import { RawTeamMembership, RawUser, } from '@typings/database/database'; -import {IsolatedEntities} from '@typings/database/enums'; -import {License} from '@typings/database/license'; -import Role from '@typings/database/role'; -import User from '@typings/database/user'; -import {createAndSetActiveDatabase, getActiveServerDatabase, getDefaultDatabase} from '@utils/database'; +import {License} from '@typings/database/models/servers/license'; +import Role from '@typings/database/models/servers/role'; +import User from '@typings/database/models/servers/user'; import {getCSRFFromCookie} from '@utils/security'; const HTTP_UNAUTHORIZED = 401; -export const logout = async (skipServerLogout = false) => { +// TODO: Requests should know the server url +// To select the right DB & Client + +export const logout = async (serverUrl: string, skipServerLogout = false) => { return async () => { if (!skipServerLogout) { try { @@ -46,39 +47,38 @@ export const logout = async (skipServerLogout = false) => { //fixme: uncomment below EventEmitter.emit // EventEmitter.emit(NavigationTypes.NAVIGATION_RESET); + DatabaseManager.deleteServerDatabase(serverUrl); + return {data: true}; }; }; -export const forceLogoutIfNecessary = async (err: Client4Error) => { - const {activeServerDatabase, error} = await getActiveServerDatabase(); - - if (!activeServerDatabase) { - return {error}; +export const forceLogoutIfNecessary = async (serverUrl: string, err: Client4Error) => { + const database = DatabaseManager.serverDatabases[serverUrl].database; + if (!database) { + return {error: `${serverUrl} database not found`}; } - const currentUserId = await getCurrentUserId(activeServerDatabase); + const currentUserId = await getCurrentUserId(database); if ('status_code' in err && err.status_code === HTTP_UNAUTHORIZED && err?.url?.indexOf('/login') === -1 && currentUserId) { - await logout(false); + await logout(serverUrl); } return {error: null}; }; -export const login = async ({ldapOnly = false, loginId, mfaToken, password}: LoginArgs) => { +export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaToken, password}: LoginArgs) => { let deviceToken; let user; - const {error, defaultDatabase} = await getDefaultDatabase(); - if (!defaultDatabase) { - return {error}; + const appDatabase = DatabaseManager.appDatabase?.database; + if (!appDatabase) { + return {error: 'App database not found.'}; } - const url = Client4.getUrl(); - try { - deviceToken = await getDeviceToken(defaultDatabase); + deviceToken = await getDeviceToken(appDatabase); user = ((await Client4.login( loginId, password, @@ -87,30 +87,33 @@ export const login = async ({ldapOnly = false, loginId, mfaToken, password}: Log ldapOnly, )) as unknown) as RawUser; - await createAndSetActiveDatabase({serverUrl: url}); - await getCSRFFromCookie(Client4.getUrl()); + await DatabaseManager.createServerDatabase({ + config: { + dbName: serverUrl, + serverUrl, + }, + }); + await DatabaseManager.setActiveServerDatabase(serverUrl); + await getCSRFFromCookie(serverUrl); } catch (e) { return {error: e}; } - const result = await loadMe({user, deviceToken}); + const result = await loadMe(serverUrl, {user, deviceToken}); if (!result?.error) { - await completeLogin(user); + await completeLogin(serverUrl, user); } - return result; + return {result}; }; -export const loadMe = async ({deviceToken, user}: LoadMeArgs) => { +export const loadMe = async (serverUrl: string, {deviceToken, user}: LoadMeArgs) => { let currentUser = user; - const {activeServerDatabase, error} = await getActiveServerDatabase(); - if (!activeServerDatabase) { - return { - error, - currentUser: undefined, - }; + const database = DatabaseManager.serverDatabases[serverUrl].database; + if (!database) { + return {error: `${serverUrl} database not found`}; } try { @@ -122,7 +125,7 @@ export const loadMe = async ({deviceToken, user}: LoadMeArgs) => { currentUser = ((await Client4.getMe()) as unknown) as RawUser; } } catch (e) { - await forceLogoutIfNecessary(e); + await forceLogoutIfNecessary(serverUrl, e); return { error: e, currentUser: undefined, @@ -161,10 +164,8 @@ export const loadMe = async ({deviceToken, user}: LoadMeArgs) => { licenseRequest, ]); - const operator = new Operator(activeServerDatabase); - + const operator = DatabaseManager.serverDatabases[serverUrl].operator; const teamRecords = operator.handleTeam({prepareRecordsOnly: true, teams: teams as RawTeam[]}); - const teamMembershipRecords = operator.handleTeamMemberships({prepareRecordsOnly: true, teamMemberships: (teamMembers as unknown) as RawTeamMembership[]}); const myTeams = teamUnreads.map((unread) => { @@ -177,16 +178,15 @@ export const loadMe = async ({deviceToken, user}: LoadMeArgs) => { myTeams: (myTeams as unknown) as RawMyTeam[], }); - const systemRecords = operator.handleIsolatedEntity({ - tableName: IsolatedEntities.SYSTEM, - values: [ + const systemRecords = operator.handleSystem({ + systems: [ { name: 'config', - value: config, + value: JSON.stringify(config), }, { name: 'license', - value: license, + value: JSON.stringify(license), }, { name: 'currentUserId', @@ -226,7 +226,7 @@ export const loadMe = async ({deviceToken, user}: LoadMeArgs) => { const rolesByName = ((await Client4.getRolesByNames(Array.from(rolesToLoad))) as unknown) as RawRole[]; if (rolesByName?.length) { - rolesRecords = operator.handleIsolatedEntity({tableName: IsolatedEntities.ROLE, prepareRecordsOnly: true, values: rolesByName}) as Role[]; + rolesRecords = await operator.handleRole({prepareRecordsOnly: true, roles: rolesByName}) as Role[]; } } @@ -234,7 +234,7 @@ export const loadMe = async ({deviceToken, user}: LoadMeArgs) => { const flattenedModels = models.flat(); if (flattenedModels?.length > 0) { - await operator.batchOperations({database: activeServerDatabase, models: flattenedModels}); + await operator.batchRecords(flattenedModels); } } catch (e) { return {error: e, currentUser: undefined}; @@ -243,46 +243,47 @@ export const loadMe = async ({deviceToken, user}: LoadMeArgs) => { return {currentUser, error: undefined}; }; -export const completeLogin = async (user: RawUser) => { - const {activeServerDatabase, error} = await getActiveServerDatabase(); - if (!activeServerDatabase) { - return {error}; +export const completeLogin = async (serverUrl: string, user: RawUser) => { + const database = DatabaseManager.serverDatabases[serverUrl].database; + if (!database) { + return {error: `${serverUrl} database not found`}; } - const {config, license}: { config: Partial; license: Partial; } = await getCommonSystemValues(activeServerDatabase); + const {config, license}: { config: Partial; license: Partial; } = await getCommonSystemValues(database); if (!Object.keys(config)?.length || !Object.keys(license)?.length) { return null; } const token = Client4.getToken(); - const serverUrl = Client4.getUrl(); setServerCredentials(serverUrl, user.id, token); // Set timezone if (isTimezoneEnabled(config)) { const timezone = getDeviceTimezone(); - await autoUpdateTimezone({deviceTimezone: timezone, userId: user.id}); + await autoUpdateTimezone(serverUrl, {deviceTimezone: timezone, userId: user.id}); } let dataRetentionPolicy: any; - const operator = new Operator(activeServerDatabase); + const operator = DatabaseManager.serverDatabases[Client4.getUrl()].operator; // Data retention if (config?.DataRetentionEnableMessageDeletion === 'true' && license?.IsLicensed === 'true' && license?.DataRetention === 'true') { - dataRetentionPolicy = await getDataRetentionPolicy(); - await operator.handleIsolatedEntity({tableName: IsolatedEntities.SYSTEM, values: [{name: 'dataRetentionPolicy', value: dataRetentionPolicy}], prepareRecordsOnly: false}); + dataRetentionPolicy = await getDataRetentionPolicy(serverUrl); + await operator.handleSystem({systems: [{name: 'dataRetentionPolicy', value: dataRetentionPolicy}], prepareRecordsOnly: false}); } return null; }; -export const updateMe = async (user: User) => { - const {activeServerDatabase, error} = await getActiveServerDatabase(); - if (!activeServerDatabase) { - return {error}; +export const updateMe = async (serverUrl: string, user: User) => { + const database = DatabaseManager.serverDatabases[serverUrl].database; + const operator = DatabaseManager.serverDatabases[serverUrl].operator; + if (!database) { + return {error: `${serverUrl} database not found`}; } + let data; try { data = ((await Client4.patchMe(user._raw)) as unknown) as RawUser; @@ -291,10 +292,8 @@ export const updateMe = async (user: User) => { return {error: e}; } - const operator = new Operator(activeServerDatabase); - const systemRecords = operator.handleIsolatedEntity({ - tableName: IsolatedEntities.SYSTEM, - values: [ + const systemRecords = operator.handleSystem({ + systems: [ {name: 'currentUserId', value: data.id}, {name: 'locale', value: data?.locale}, ], @@ -303,34 +302,33 @@ export const updateMe = async (user: User) => { const userRecord = operator.handleUsers({prepareRecordsOnly: true, users: [data]}); - //todo: ?? Do we need to write to TOS entity ? See app/mm-redux/reducers/entities/users.ts/profiles/line 152 const + //todo: ?? Do we need to write to TOS table ? See app/mm-redux/reducers/entities/users.ts/profiles/line 152 const // tosRecords = await DataOperator.handleIsolatedEntity({ tableName: TERMS_OF_SERVICE, values: [{}], }); const models = await Promise.all([ - ...systemRecords, - ...userRecord, + systemRecords, + userRecord, // ...tosRecords, ]); if (models?.length) { - await operator.batchOperations({database: activeServerDatabase, models: models.flat()}); + await operator.batchRecords(models.flat()); } const updatedRoles: string[] = data.roles.split(' '); if (updatedRoles.length) { - await loadRolesIfNeeded(updatedRoles); + await loadRolesIfNeeded(serverUrl, updatedRoles); } return {data}; }; - -export const getSessions = async (currentUserId: string) => { +export const getSessions = async (serverUrl: string, currentUserId: string) => { try { const sessions = await Client4.getSessions(currentUserId); - await createSessions(sessions); + await createSessions(serverUrl, sessions); } catch (e) { logError(e); - await forceLogoutIfNecessary(e); + await forceLogoutIfNecessary(serverUrl, e); } }; @@ -342,16 +340,22 @@ type LoadedUser = { export const ssoLogin = async (serverUrl: string) => { let deviceToken; - const {error, defaultDatabase} = await getDefaultDatabase(); + const database = DatabaseManager.appDatabase?.database; - if (!defaultDatabase) { - return {error}; + if (!database) { + return {error: 'App database not found'}; } // Setting up active database for this SSO login flow try { - await createAndSetActiveDatabase({serverUrl}); - deviceToken = await getDeviceToken(defaultDatabase); + await DatabaseManager.createServerDatabase({ + config: { + dbName: serverUrl, + serverUrl, + }, + }); + await DatabaseManager.setActiveServerDatabase(serverUrl); + deviceToken = await getDeviceToken(database); } catch (e) { return {error: e}; } @@ -359,9 +363,9 @@ export const ssoLogin = async (serverUrl: string) => { let result; try { - result = await loadMe({deviceToken}) as unknown as LoadedUser; + result = await loadMe(serverUrl, {deviceToken}) as unknown as LoadedUser; if (!result?.error && result?.currentUser) { - await completeLogin(result.currentUser); + await completeLogin(serverUrl, result.currentUser); } } catch (e) { return {error: e}; diff --git a/app/screens/login/index.tsx b/app/screens/login/index.tsx index e6a4e0f62a..586a0bdfce 100644 --- a/app/screens/login/index.tsx +++ b/app/screens/login/index.tsx @@ -36,12 +36,13 @@ type LoginProps = { componentId: string; config: ClientConfig; license: ClientLicense; + serverUrl: string; theme: Theme; }; export const MFA_EXPECTED_ERRORS = ['mfa.validate_token.authenticate.app_error', 'ent.mfa.validate_token.authenticate.app_error']; -const Login: NavigationFunctionComponent = ({config, license, theme}: LoginProps) => { +const Login: NavigationFunctionComponent = ({config, license, serverUrl, theme}: LoginProps) => { const styles = getStyleSheet(theme); const loginRef = useRef(null); @@ -128,15 +129,15 @@ const Login: NavigationFunctionComponent = ({config, license, theme}: LoginProps }); const signIn = async () => { - const result = await login({loginId: loginId.toLowerCase(), password, config, license}); + const result = await login(serverUrl, {loginId: loginId.toLowerCase(), password, config, license}); if (checkLoginResponse(result)) { await goToChannel(); } }; const goToChannel = async () => { - await scheduleExpiredNotification(intl); - resetToChannel(); + await scheduleExpiredNotification(serverUrl, intl); + resetToChannel({serverUrl}); }; const checkLoginResponse = (data: any) => { @@ -160,7 +161,7 @@ const Login: NavigationFunctionComponent = ({config, license, theme}: LoginProps const goToMfa = () => { const screen = MFA; const title = intl.formatMessage({id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'}); - goToScreen(screen, title, {goToChannel, loginId, password, config, license, theme}); + goToScreen(screen, title, {goToChannel, loginId, password, config, license, serverUrl, theme}); }; const getLoginErrorMessage = (loginError: any) => { diff --git a/app/screens/login/login.test.tsx b/app/screens/login/login.test.tsx index a5e9c21007..c04b74cd27 100644 --- a/app/screens/login/login.test.tsx +++ b/app/screens/login/login.test.tsx @@ -33,6 +33,7 @@ describe('Login', () => { IsLicensed: 'false', }, theme: Preferences.THEMES.default, + serverUrl: 'https://locahost:8065', }; test('Login screen should match snapshot', () => { @@ -118,6 +119,7 @@ describe('Login', () => { password, config: {EnableSignInWithEmail: 'true', EnableSignInWithUsername: 'true'}, license: {IsLicensed: 'false'}, + serverUrl: baseProps.serverUrl, theme: baseProps.theme, }, ); diff --git a/app/screens/login_options/index.tsx b/app/screens/login_options/index.tsx index 86e86cc4ef..4cd6f85ce0 100644 --- a/app/screens/login_options/index.tsx +++ b/app/screens/login_options/index.tsx @@ -57,7 +57,7 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({ }, })); -const LoginOptions: NavigationFunctionComponent = ({config, license, theme, serverUrl}: LoginOptionsProps) => { +const LoginOptions: NavigationFunctionComponent = ({config, license, serverUrl, theme}: LoginOptionsProps) => { const intl = useIntl(); const styles = getStyles(theme); @@ -65,7 +65,7 @@ const LoginOptions: NavigationFunctionComponent = ({config, license, theme, serv const screen = LOGIN; const title = intl.formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'}); - goToScreen(screen, title, {config, license, theme}); + goToScreen(screen, title, {config, license, serverUrl, theme}); }); const displaySSO = preventDoubleTap((ssoType: string) => { diff --git a/app/screens/mfa/index.tsx b/app/screens/mfa/index.tsx index 6432b74204..21ae5d9af6 100644 --- a/app/screens/mfa/index.tsx +++ b/app/screens/mfa/index.tsx @@ -19,8 +19,8 @@ import {SafeAreaView} from 'react-native-safe-area-context'; import ErrorText from '@components/error_text'; import FormattedText from '@components/formatted_text'; import {login} from '@requests/remote/user'; -import {Config} from '@typings/database/config'; -import {License} from '@typings/database/license'; +import {Config} from '@typings/database/models/servers/config'; +import {License} from '@typings/database/models/servers/license'; import {t} from '@utils/i18n'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; @@ -31,10 +31,11 @@ type MFAProps = { license: Partial, loginId : string, password: string, + serverUrl: string; theme: Theme; } -const MFA = ({config, goToChannel, license, loginId, password, theme}: MFAProps) => { +const MFA = ({config, goToChannel, license, loginId, password, serverUrl, theme}: MFAProps) => { const [token, setToken] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); @@ -75,7 +76,7 @@ const MFA = ({config, goToChannel, license, loginId, password, theme}: MFAProps) return; } setIsLoading(true); - const result = await login({loginId, password, mfaToken: token, config, license}); + const result = await login(serverUrl, {loginId, password, mfaToken: token, config, license}); setIsLoading(false); if (result?.error) { setError(result?.error); diff --git a/app/screens/mfa/mfa.test.tsx b/app/screens/mfa/mfa.test.tsx index 4cd2595130..88a5c2ee71 100644 --- a/app/screens/mfa/mfa.test.tsx +++ b/app/screens/mfa/mfa.test.tsx @@ -22,6 +22,7 @@ describe('*** MFA Screen ***', () => { loginId: 'loginId', password: 'passwd', license: {}, + serverUrl: 'https://locahost:8065', theme: Preferences.THEMES.default, }; diff --git a/app/screens/sso/index.tsx b/app/screens/sso/index.tsx index 29cde6c598..1266d36094 100644 --- a/app/screens/sso/index.tsx +++ b/app/screens/sso/index.tsx @@ -83,7 +83,7 @@ const SSO = ({config, serverUrl, ssoType, theme}: SSOProps) => { }; const goToChannel = () => { - scheduleExpiredNotification(intl); + scheduleExpiredNotification(serverUrl, intl); resetToChannel(); }; diff --git a/app/screens/sso/sso.test.tsx b/app/screens/sso/sso.test.tsx index 50b3e1ea8b..f75c91fbbe 100644 --- a/app/screens/sso/sso.test.tsx +++ b/app/screens/sso/sso.test.tsx @@ -27,7 +27,7 @@ describe('SSO', () => { }, ssoType: 'GITLAB', theme: Preferences.THEMES.default, - serverUrl: 'https://rc.test.mattermost.com', + serverUrl: 'https://locahost:8065', }; test('implement with webview when version is less than 5.32 version', async () => { diff --git a/app/screens/sso/sso_with_redirect_url.tsx b/app/screens/sso/sso_with_redirect_url.tsx index 5162cedbda..8322623661 100644 --- a/app/screens/sso/sso_with_redirect_url.tsx +++ b/app/screens/sso/sso_with_redirect_url.tsx @@ -10,7 +10,6 @@ import urlParse from 'url-parse'; import FormattedText from '@components/formatted_text'; import Loading from '@components/loading'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; -import {setDeepLinkUrl} from '@requests/local/systems'; import {tryOpenURL} from '@utils/url'; interface SSOWithRedirectURLProps { @@ -67,8 +66,6 @@ const SSOWithRedirectURL = ({loginError, loginUrl, onCSRFToken, onMMToken, setLo useEffect(() => { const onURLChange = ({url}: { url: string }) => { if (url && url.startsWith(redirectUrl)) { - // save deepLinkUrl under Global - setDeepLinkUrl(''); const parsedUrl = urlParse(url, true); if (parsedUrl.query && parsedUrl.query.MMCSRF && parsedUrl.query.MMAUTHTOKEN) { onCSRFToken(parsedUrl.query.MMCSRF); diff --git a/app/utils/database.ts b/app/utils/database.ts deleted file mode 100644 index fc739b7d4d..0000000000 --- a/app/utils/database.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import urlParse from 'url-parse'; - -import DatabaseConnectionException from '@database/exceptions/database_connection_exception'; -import DatabaseManager from '@database/manager'; - -type SetActiveDatabaseArgs = { - serverUrl: string; - displayName?: string; -}; - -export const createAndSetActiveDatabase = async ({serverUrl, displayName}: SetActiveDatabaseArgs) => { - const connectionName = displayName ?? urlParse(serverUrl)?.hostname; - - try { - const databaseClient = new DatabaseManager(); - await databaseClient.getDatabaseConnection({serverUrl, connectionName, setAsActiveDatabase: true}); - } catch (e) { - throw new DatabaseConnectionException( - `createAndSetActiveDatabase: Unable to create and set serverUrl ${serverUrl} as current active database with name ${displayName}`, - ); - } -}; - -export const getDefaultDatabase = async () => { - try { - const databaseClient = new DatabaseManager(); - const defaultDatabase = await databaseClient.getDefaultDatabase(); - return { - error: defaultDatabase ? null : 'Unable to retrieve the App database.', - defaultDatabase, - }; - } catch (e) { - return { - error: 'Unable to retrieve the App database.', - defaultDatabase: null, - }; - } -}; - -export const getActiveServerDatabase = async () => { - try { - const databaseClient = new DatabaseManager(); - const activeServerDatabase = await databaseClient.getActiveServerDatabase(); - - return { - error: activeServerDatabase ? null : 'Unable to retrieve the current active server database.', - activeServerDatabase, - }; - } catch (e) { - return { - error: 'Unable to retrieve the current active server database.', - activeServerDatabase: null, - }; - } -}; diff --git a/app/utils/security.ts b/app/utils/security.ts index 94354b2bbd..b2dc83c1bb 100644 --- a/app/utils/security.ts +++ b/app/utils/security.ts @@ -7,3 +7,19 @@ export async function getCSRFFromCookie(url: string) { const cookies = await CookieManager.get(url, false); return cookies.MMCSRF?.value; } + +export const hashCode = (str: string): string => { + let hash = 0; + let i; + let chr; + if (!str || str.length === 0) { + return hash.toString(); + } + + for (i = 0; i < str.length; i++) { + chr = str.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash |= 0; // Convert to 32bit integer + } + return hash.toString(); +}; diff --git a/package-lock.json b/package-lock.json index 95f9b01ac3..23a46e9070 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3415,13 +3415,6 @@ "rambdax": "2.15.0", "rxjs": "^6.5.3", "sql-escape-string": "^1.1.0" - }, - "dependencies": { - "lokijs": { - "version": "npm:@nozbe/lokijs@1.5.12-wmelon", - "resolved": "https://registry.npmjs.org/@nozbe/lokijs/-/lokijs-1.5.12-wmelon.tgz", - "integrity": "sha512-7xQUn80pzPBB9VcwvB/W2V9/60xIfuk+3IDIvS9cU7W29jJx4QBXe5dBWTaARmxD9hXozPCcPWh2wfd7m4dbTA==" - } } }, "@nozbe/with-observables": { @@ -16601,6 +16594,11 @@ } } }, + "lokijs": { + "version": "npm:@nozbe/lokijs@1.5.12-wmelon", + "resolved": "https://registry.npmjs.org/@nozbe/lokijs/-/lokijs-1.5.12-wmelon.tgz", + "integrity": "sha512-7xQUn80pzPBB9VcwvB/W2V9/60xIfuk+3IDIvS9cU7W29jJx4QBXe5dBWTaARmxD9hXozPCcPWh2wfd7m4dbTA==" + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", diff --git a/test/setup.ts b/test/setup.ts index f73d1fdca8..efe303d0df 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -11,14 +11,12 @@ require('react-native-reanimated/lib/reanimated2/jestUtils').setUpTests(); require('isomorphic-fetch'); -const mockImpl = new MockAsyncStorage(); -jest.mock('@react-native-community/async-storage', () => mockImpl); - // @ts-expect-error no window exist in global global.window = {}; /* eslint-disable no-console */ - +jest.mock('@react-native-community/async-storage', () => new MockAsyncStorage()); +jest.mock('@database/manager'); jest.doMock('react-native', () => { const { Platform, @@ -89,6 +87,15 @@ jest.doMock('react-native', () => { Appearance: { getColorScheme: jest.fn().mockReturnValue('light'), }, + MattermostManaged: { + getConstants: () => ({ + appGroupIdentifier: 'group.mattermost.rnbeta', + appGroupSharedDirectory: { + sharedDirectory: '', + databasePath: '', + }, + }), + }, }; const Linking = { diff --git a/types/database/database.d.ts b/types/database/database.d.ts index 25ece2deda..bc31c172e9 100644 --- a/types/database/database.d.ts +++ b/types/database/database.d.ts @@ -1,15 +1,20 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +/* eslint-disable max-lines */ + import {Database} from '@nozbe/watermelondb'; import Model from '@nozbe/watermelondb/Model'; import {Clause} from '@nozbe/watermelondb/QueryDescription'; import {Class} from '@nozbe/watermelondb/utils/common'; -import {Config} from '@typings/database/config'; -import {License} from '@typings/database/license'; -import System from '@typings/database/system'; -import {DatabaseType, IsolatedEntities} from './enums'; +import type AppDataOperator from '@database/operator/app_data_operator'; +import type ServerDataOperator from '@app/database/operator/server_data_operator'; +import type {Config} from '@typings/database/models/servers/config'; +import type {License} from '@typings/database/models/servers/license'; +import type System from '@typings/database/models/servers/system'; + +import {DatabaseType} from './enums'; export type MigrationEvents = { onSuccess: () => void; @@ -17,511 +22,55 @@ export type MigrationEvents = { onFailure: (error: string) => void; }; -export type DatabaseConfigs = { - actionsEnabled?: boolean; +export type CreateServerDatabaseConfig = { dbName: string; dbType?: DatabaseType.DEFAULT | DatabaseType.SERVER; + displayName?: string; serverUrl?: string; }; -export type DefaultNewServerArgs = { +export type RegisterServerDatabaseArgs = { databaseFilePath: string; displayName: string; serverUrl: string; }; -// A database connection is of type 'Database'; unless it fails to be initialize and in which case it becomes 'undefined' -export type DatabaseInstance = Database | undefined; - -export type RawApp = { - build_number: string; - created_at: number; - version_number: string; +export type AppDatabase = { + database: Database; + operator: AppDataOperator; }; -export type RawGlobal = { - name: string; - value: string; -}; - -export type RawServers = { - db_path: string; - display_name: string; - mention_count: number; - unread_count: number; - url: string; - isSecured: boolean; - lastActiveAt: number; -}; - -export type RawCustomEmoji = { - id: string; - name: string; - create_at?: number; - update_at?: number; - delete_at?: number; - creator_id: string; -}; - -export type RawRole = { - id: string; - name: string; - display_name?: string; - description?: string; - permissions: string[]; - scheme_managed?: boolean; -}; - -export type RawSystem = { - id: string; - name: string; - value: string; -}; - -export type RawTermsOfService = { - id: string; - accepted_at: number; - create_at: number; - user_id: string; - text: string; -}; - -export type RawDraft = { - channel_id: string; - files?: FileInfo[]; - message?: string; - root_id: string; -}; - -export type RawEmbed = { data: {}; type: string; url: string }; - -export type RawPostMetadata = { - data: any; - type: string; - postId: string; -}; - -interface PostMetadataTypes { - embeds: PostEmbed; - images: Dictionary; +export type ServerDatabase = { + database: Database; + operator: ServerDataOperator; } -export type RawFile = { - create_at: number; - delete_at: number; - extension: string; - has_preview_image?: boolean; - height: number; - id?: string; - localPath?: string; - mime_type?: string; - mini_preview?: string; // thumbnail - name: string; - post_id: string; - size: number; - update_at: number; - user_id: string; - width?: number; +export type ServerDatabases = { + [x: string]: ServerDatabase; }; -export type RawReaction = { - id? : string; - create_at: number; - delete_at: number; - emoji_name: string; - post_id: string; - update_at: number; - user_id: string; -}; - -export type RawPostsInChannel = { - channel_id: string; - earliest: number; - latest: number; -}; - -interface PostEmbed { - type: PostEmbedType; - url: string; - data: Record; -} - -interface PostImage { - height: number; - width: number; - format?: string; - frame_count?: number; -} - -interface PostImageMetadata extends PostImage { - url: string; -} - -export type PostMetadataData = Record | PostImageMetadata; - -export type PostMetadataType = 'images' | 'embeds'; - -// The RawPost describes the shape of the object received from a getPosts request -export type RawPost = { - channel_id: string; - create_at: number; - delete_at: number; - edit_at: number; - file_ids?: string[]; - filenames?: string[]; - hashtags: string; - id: string; - is_pinned?: boolean; - last_reply_at?: number; - message: string; - original_id: string; - parent_id: string; - participants?: null; - pending_post_id: string; - prev_post_id?: string; // taken from getPosts API call; outside of post object - props: object; - reply_count?: number; - root_id: string; - type: string; - update_at: number; - user_id: string; - metadata?: { - embeds?: RawEmbed[]; - emojis?: RawCustomEmoji[]; - files?: RawFile[]; - images?: Dictionary; - reactions?: RawReaction[]; - }; -}; - -export type RawUser = { - id: string; - auth_service: string; - create_at: number; - delete_at: number; - email: string; - email_verified: boolean; - failed_attempts?: number; - first_name: string; - is_bot: boolean; - last_name: string; - last_password_update: number; - last_picture_update: number; - locale: string; - mfa_active?: boolean; - nickname: string; - notify_props: { - channel: boolean; - desktop: string; - desktop_sound: boolean; - email: boolean; - first_name: boolean; - mention_keys: string; - push: string; - auto_responder_active: boolean; - auto_responder_message: string; - desktop_notification_sound: string; // Not in use by the mobile app - push_status: string; - comments: string; - }; - position?: string; - props: UserProps; - roles: string; - timezone: { - useAutomaticTimezone: string; - manualTimezone: string; - automaticTimezone: string; - }; - terms_of_service_create_at?: number; - terms_of_service_id?: string; - update_at: number; - username: string; -}; - -export type RawPreference = { - category: string; - name: string; - user_id: string; - value: string; -}; - -export type RawTeamMembership = { - id? : string; - delete_at: number; - explicit_roles: string; - roles: string; - scheme_admin: boolean; - scheme_guest: boolean; - scheme_user: boolean; - team_id: string; - user_id: string; -}; - -export type RawGroupMembership = { - id?: string; - user_id: string; - group_id: string; -}; - -export type RawChannelMembership = { - id? : string; - channel_id: string; - user_id: string; - roles: string; - last_viewed_at: number; - msg_count: number; - mention_count: number; - notify_props: { - desktop: string; - email: string; - ignore_channel_mentions: string; - mark_unread: string; - push: string; - }; - last_update_at: number; - scheme_guest: boolean; - scheme_user: boolean; - scheme_admin: boolean; - explicit_roles: string; -}; - -export type RawChannelMembers = { - channel_id: string; - explicit_roles: string; - last_update_at: number; - last_viewed_at: number; - mention_count: number; - msg_count: number; - notify_props: NotifyProps; - roles: string; - scheme_admin: boolean; - scheme_guest: boolean; - scheme_user: boolean; - user_id: string; -}; - -export type RawPostsInThread = { - earliest: number; - latest?: number; - post_id: string; -}; - -export type RawGroup = { - create_at: number; - delete_at: number; - description: string; - display_name: string; - has_syncables: boolean; - id: string; - name: string; - remote_id: string; - source: string; - update_at: number; -}; - -export type RawGroupsInTeam = { - auto_add: boolean; - create_at: number; - delete_at: number; - group_id: string; - team_display_name: string; - team_id: string; - team_type: string; - update_at: number; -}; - -export type RawGroupsInChannel = { - auto_add: boolean; - channel_display_name: string; - channel_id: string; - channel_type: string; - create_at: number; - delete_at: number; - group_id: string; - team_display_name: string; - team_id: string; - team_type: string; - update_at: number; - member_count: number; - timezone_count: number; -}; - -export type RawTeam = { - id: string; - allow_open_invite: boolean; - allowed_domains: string; - company_name: string; - create_at: number; - delete_at: number; - description: string; - display_name: string; - email: string; - group_constrained: boolean | null; - invite_id: string; - last_team_icon_update: number; - name: string; - scheme_id: string; - type: string; - update_at: number; -}; - -export type RawTeamChannelHistory = { - team_id: string; - channel_ids: string[]; -}; - -export type RawTeamSearchHistory = { - created_at: number; - display_term: string; - term: string; - team_id: string; -}; - -export type RawSlashCommand = { - id: string; - auto_complete: boolean; - auto_complete_desc: string; - auto_complete_hint: string; - create_at: number; - creator_id: string; - delete_at: number; - description: string; - display_name: string; - icon_url: string; - method: string; - team_id: string; - token: string; - trigger: string; - update_at: number; - url: string; - username: string; -}; - -export type RawMyTeam = { - team_id: string; - roles: string; - is_unread: boolean; - mentions_count: number; -}; - -export type ChannelType = 'D' | 'O' | 'G' | 'P'; - -export type RawChannel = { - create_at: number; - creator_id: string; - delete_at: number; - display_name: string; - extra_update_at: number; - group_constrained: boolean | null; - header: string; - id: string; - last_post_at: number; - name: string; - props: null; - purpose: string; - scheme_id: null; - shared: null; - team_id: string; - total_msg_count: number; - type: ChannelType; - update_at: number; -}; - -export type RawMyChannelSettings = { - notify_props: NotifyProps; - channel_id: string; -}; - -export type RawChannelInfo = { - channel_id: string; - guest_count: number; - header: string; - member_count: number; - pinned_post_count: number; - purpose: string; -}; - -export type RawMyChannel = { - channel_id: string; - last_post_at: number; - last_viewed_at: number; - mentions_count: number; - message_count: number; - roles: string; -}; - -export type RawValue = - | RawApp - | RawChannel - | RawChannelInfo - | RawChannelMembership - | RawCustomEmoji - | RawDraft - | RawFile - | RawGlobal - | RawGroup - | RawGroupMembership - | RawGroupsInChannel - | RawGroupsInTeam - | RawMyChannel - | RawMyChannelSettings - | RawMyTeam - | RawPost - | RawPostMetadata - | RawPostsInChannel - | RawPostsInThread - | RawPreference - | RawReaction - | RawRole - | RawServers - | RawSlashCommand - | RawSystem - | RawTeam - | RawTeamChannelHistory - | RawTeamMembership - | RawTeamSearchHistory - | RawTermsOfService - | RawUser; - -export type DataFactoryArgs = { +export type TransformerArgs = { action: string; database: Database; - generator?: (model: Model) => void; + fieldsMapper?: (model: Model) => void; tableName?: string; - value: MatchExistingRecord; + value: RecordPair; }; -export type PrepareForDatabaseArgs = { +export type OperationArgs = { tableName: string; - createRaws?: MatchExistingRecord[]; - updateRaws?: MatchExistingRecord[]; - recordOperator: (DataFactoryArgs) => Promise; -}; - -export type PrepareRecordsArgs = PrepareForDatabaseArgs & { - database: Database; -}; - -export type BatchOperationsArgs = { database: Database; models: Model[] }; - -export type HandleIsolatedEntityArgs = { - tableName: IsolatedEntities; - values: RawValue[]; - prepareRecordsOnly: boolean; + createRaws?: RecordPair[]; + updateRaws?: RecordPair[]; + deleteRaws?: Model[]; + transformer: (TransformerArgs) => Promise; }; export type Models = Class[]; -// The elements needed to create a new connection -export type DatabaseConnectionArgs = { - configs: DatabaseConfigs; - shouldAddToDefaultDatabase: boolean; -}; - -// The elements required to switch to another active server database -export type ActiveServerDatabaseArgs = { - displayName: string; - serverUrl: string; +// The elements needed to create a new database +export type CreateServerDatabaseArgs = { + config: CreateServerDatabaseConfig; + shouldAddToAppDatabase?: boolean; }; export type HandleReactionsArgs = { @@ -575,27 +124,24 @@ export type RetrieveRecordsArgs = { condition: Clause; }; -export type ProcessInputsArgs = { - rawValues: RawValue[]; +export type ProcessRecordsArgs = { + createOrUpdateRawValues: RawValue[]; + deleteRawValues: RawValue[]; tableName: string; fieldName: string; findMatchingRecordBy: (existing: Model, newElement: RawValue) => boolean; }; -export type HandleEntityRecordsArgs = { +export type HandleRecordsArgs = { findMatchingRecordBy: (existing: Model, newElement: RawValue) => boolean; fieldName: string; - operator: (DataFactoryArgs) => Promise; - rawValues: RawValue[]; + transformer: (TransformerArgs) => Promise; + createOrUpdateRawValues: RawValue[]; + deleteRawValues?: RawValue[]; tableName: string; prepareRecordsOnly: boolean; }; -export type DatabaseInstances = { - dbInstance: DatabaseInstance; - url: string; -}; - export type RangeOfValueArgs = { raws: RawValue[]; fieldName: string; @@ -606,88 +152,106 @@ export type RecordPair = { raw: RawValue; }; -export type HandleMyChannelArgs = { +type PrepareOnly = { + prepareRecordsOnly: boolean; +} + +export type HandleInfoArgs = PrepareOnly & { + info: RawInfo[] +} +export type HandleServersArgs = PrepareOnly & { + servers: RawServers[] +} +export type HandleGlobalArgs = PrepareOnly & { + global: RawGlobal[] +} + +export type HandleRoleArgs = PrepareOnly & { + roles: RawRole[] +} + +export type HandleCustomEmojiArgs = PrepareOnly & { + emojis: RawCustomEmoji[] +} + +export type HandleSystemArgs = PrepareOnly & { + systems: RawSystem[] +} + +export type HandleTOSArgs = PrepareOnly & { + termOfService: RawTermsOfService[] +} + +export type HandleMyChannelArgs = PrepareOnly & { myChannels: RawMyChannel[]; - prepareRecordsOnly: boolean; }; -export type HandleChannelInfoArgs = { +export type HandleChannelInfoArgs = PrepareOnly &{ channelInfos: RawChannelInfo[]; - prepareRecordsOnly: boolean; }; -export type HandleMyChannelSettingsArgs = { +export type HandleMyChannelSettingsArgs = PrepareOnly & { settings: RawMyChannelSettings[]; - prepareRecordsOnly: boolean; }; -export type HandleChannelArgs = { +export type HandleChannelArgs = PrepareOnly & { channels: RawChannel[]; - prepareRecordsOnly: boolean; }; -export type HandleMyTeamArgs = { +export type HandleMyTeamArgs = PrepareOnly & { myTeams: RawMyTeam[]; - prepareRecordsOnly: boolean; }; -export type HandleSlashCommandArgs = { - slashCommands: RawSlashCommand[]; - prepareRecordsOnly: boolean; +export type HandleSlashCommandArgs = PrepareOnly & { + slashCommands: RawSlashCommand[]; }; -export type HandleTeamSearchHistoryArgs = { +export type HandleTeamSearchHistoryArgs = PrepareOnly &{ teamSearchHistories: RawTeamSearchHistory[]; - prepareRecordsOnly: boolean; }; -export type HandleTeamChannelHistoryArgs = { +export type HandleTeamChannelHistoryArgs = PrepareOnly & { teamChannelHistories: RawTeamChannelHistory[]; - prepareRecordsOnly: boolean; }; -export type HandleTeamArgs = { teams: RawTeam[]; prepareRecordsOnly: boolean }; +export type HandleTeamArgs = PrepareOnly & { + teams: RawTeam[]; +}; -export type HandleGroupsInChannelArgs = { +export type HandleGroupsInChannelArgs = PrepareOnly & { groupsInChannels: RawGroupsInChannel[]; - prepareRecordsOnly: boolean; }; -export type HandleGroupsInTeamArgs = { +export type HandleGroupsInTeamArgs = PrepareOnly &{ groupsInTeams: RawGroupsInTeam[]; - prepareRecordsOnly: boolean; }; -export type HandleGroupArgs = { +export type HandleGroupArgs = PrepareOnly & { groups: RawGroup[]; - prepareRecordsOnly: boolean; }; -export type HandleChannelMembershipArgs = { +export type HandleChannelMembershipArgs = PrepareOnly & { channelMemberships: RawChannelMembership[]; - prepareRecordsOnly: boolean; }; -export type HandleGroupMembershipArgs = { +export type HandleGroupMembershipArgs = PrepareOnly & { groupMemberships: RawGroupMembership[]; - prepareRecordsOnly: boolean; }; -export type HandleTeamMembershipArgs = { +export type HandleTeamMembershipArgs = PrepareOnly & { teamMemberships: RawTeamMembership[]; - prepareRecordsOnly: boolean; }; -export type HandlePreferencesArgs = { +export type HandlePreferencesArgs = PrepareOnly & { preferences: RawPreference[]; - prepareRecordsOnly: boolean; }; -export type HandleUsersArgs = { users: RawUser[]; prepareRecordsOnly: boolean }; +export type HandleUsersArgs = PrepareOnly & { + users: RawUser[]; + }; -export type HandleDraftArgs = { +export type HandleDraftArgs = PrepareOnly & { drafts: RawDraft[]; - prepareRecordsOnly: boolean; }; export type LoginArgs = { @@ -708,19 +272,449 @@ export type ServerUrlChangedArgs = { serverUrl: string; }; -export type RetrievedDatabase = { - dbInstance: DatabaseInstance; - displayName: string; - url: string; -} - export type GetDatabaseConnectionArgs = { serverUrl: string; connectionName?: string; setAsActiveDatabase: boolean; } -export type MostRecentConnection = { - connection: DatabaseInstance, - serverUrl: string, +export type ProcessRecordResults = { + createRaws: RecordPair[]; + updateRaws: RecordPair[]; + deleteRaws: Model[]; } + +export type RawGlobal = { + name: string; + value: string; +}; + +export type RawInfo = { + build_number: string; + created_at: number; + version_number: string; +}; + +export type RawServers = { + db_path: string; + display_name: string; + mention_count: number; + unread_count: number; + url: string; + isSecured: boolean; + lastActiveAt: number; +}; + +export type RawChannelInfo = { + channel_id: string; + guest_count: number; + header: string; + member_count: number; + pinned_post_count: number; + purpose: string; +}; + +export type RawChannelMembership = { + id? : string; + channel_id: string; + user_id: string; + roles: string; + last_viewed_at: number; + msg_count: number; + mention_count: number; + notify_props: { + desktop: string; + email: string; + ignore_channel_mentions: string; + mark_unread: string; + push: string; + }; + last_update_at: number; + scheme_guest: boolean; + scheme_user: boolean; + scheme_admin: boolean; + explicit_roles: string; +}; + +export type ChannelType = 'D' | 'O' | 'G' | 'P'; + +export type RawChannel = { + create_at: number; + creator_id: string; + delete_at: number; + display_name: string; + extra_update_at: number; + group_constrained: boolean | null; + header: string; + id: string; + last_post_at: number; + name: string; + props: Record | null; + purpose: string; + scheme_id: string | null; + shared: boolean | null; + team_id: string; + total_msg_count: number; + type: ChannelType; + update_at: number; +}; + +export type RawCustomEmoji = { + id: string; + name: string; + create_at?: number; + update_at?: number; + delete_at?: number; + creator_id: string; +}; + +export type RawDraft = { + channel_id: string; + files?: FileInfo[]; + message?: string; + root_id: string; +}; + +export type RawFile = { + create_at: number; + delete_at: number; + extension: string; + has_preview_image?: boolean; + height: number; + id?: string; + localPath?: string; + mime_type?: string; + mini_preview?: string; // thumbnail + name: string; + post_id: string; + size: number; + update_at: number; + user_id: string; + width?: number; +}; + +export type RawGroupMembership = { + id?: string; + user_id: string; + group_id: string; +}; + +export type RawGroup = { + create_at: number; + delete_at: number; + description: string; + display_name: string; + has_syncables: boolean; + id: string; + name: string; + remote_id: string; + source: string; + update_at: number; +}; + +export type RawGroupsInChannel = { + auto_add: boolean; + channel_display_name: string; + channel_id: string; + channel_type: string; + create_at: number; + delete_at: number; + group_id: string; + team_display_name: string; + team_id: string; + team_type: string; + update_at: number; + member_count: number; + timezone_count: number; +}; + +export type RawGroupsInTeam = { + auto_add: boolean; + create_at: number; + delete_at: number; + group_id: string; + team_display_name: string; + team_id: string; + team_type: string; + update_at: number; +}; + +export type RawMyChannelSettings = { + notify_props: NotifyProps; + channel_id: string; +}; + +export type RawMyChannel = { + channel_id: string; + last_post_at: number; + last_viewed_at: number; + mentions_count: number; + message_count: number; + roles: string; +}; + +export type RawMyTeam = { + team_id: string; + roles: string; + is_unread: boolean; + mentions_count: number; +}; + +export type RawEmbed = { data: {}; type: string; url: string }; + +export type RawPostMetadata = { + data: any; + type: string; + postId: string; +}; + +export interface PostMetadataTypes { + embeds: PostEmbed; + images: Dictionary; +} + +export interface PostEmbed { + type: PostEmbedType; + url: string; + data: Record; +} + +export interface PostImage { + height: number; + width: number; + format?: string; + frame_count?: number; +} + +export interface PostImageMetadata extends PostImage { + url: string; +} + +export type PostMetadataData = Record | PostImageMetadata; + +export type PostMetadataType = 'images' | 'embeds'; + +// The RawPost describes the shape of the object received from a getPosts request +export type RawPost = { + channel_id: string; + create_at: number; + delete_at: number; + edit_at: number; + file_ids?: string[]; + filenames?: string[]; + hashtags: string; + id: string; + is_pinned?: boolean; + last_reply_at?: number; + message: string; + original_id: string; + parent_id: string; + participants?: null; + pending_post_id: string; + prev_post_id?: string; // taken from getPosts API call; outside of post object + props: object; + reply_count?: number; + root_id: string; + type: string; + update_at: number; + user_id: string; + metadata?: { + embeds?: RawEmbed[]; + emojis?: RawCustomEmoji[]; + files?: RawFile[]; + images?: Dictionary; + reactions?: RawReaction[]; + }; +}; + +export type RawPostsInChannel = { + channel_id: string; + earliest: number; + latest: number; +}; + +export type RawPostsInThread = { + earliest: number; + latest?: number; + post_id: string; +}; + +export type RawPreference = { + category: string; + name: string; + user_id: string; + value: string; +}; + +export type RawReaction = { + id? : string; + create_at: number; + delete_at: number; + emoji_name: string; + post_id: string; + update_at: number; + user_id: string; +}; + +export type RawRole = { + id: string; + name: string; + display_name?: string; + description?: string; + permissions: string[]; + scheme_managed?: boolean; +}; + +export type RawSlashCommand = { + id: string; + auto_complete: boolean; + auto_complete_desc: string; + auto_complete_hint: string; + create_at: number; + creator_id: string; + delete_at: number; + description: string; + display_name: string; + icon_url: string; + method: string; + team_id: string; + token: string; + trigger: string; + update_at: number; + url: string; + username: string; +}; + +export type RawSystem = { + id?: string; + name: string; + value: string; +}; + +export type RawTeamChannelHistory = { + team_id: string; + channel_ids: string[]; +}; + +export type RawTeamMembership = { + id? : string; + delete_at: number; + explicit_roles: string; + roles: string; + scheme_admin: boolean; + scheme_guest: boolean; + scheme_user: boolean; + team_id: string; + user_id: string; +}; + +export type RawTeamSearchHistory = { + created_at: number; + display_term: string; + term: string; + team_id: string; +}; + +export type RawTeam = { + id: string; + allow_open_invite: boolean; + allowed_domains: string; + company_name: string; + create_at: number; + delete_at: number; + description: string; + display_name: string; + email: string; + group_constrained: boolean | null; + invite_id: string; + last_team_icon_update: number; + name: string; + scheme_id: string; + type: string; + update_at: number; +}; + +export type RawTermsOfService = { + id: string; + accepted_at: number; + create_at: number; + user_id: string; + text: string; +}; + +export type RawUser = { + id: string; + auth_service: string; + create_at: number; + delete_at: number; + email: string; + email_verified: boolean; + failed_attempts?: number; + first_name: string; + is_bot: boolean; + last_name: string; + last_password_update: number; + last_picture_update: number; + locale: string; + mfa_active?: boolean; + nickname: string; + notify_props: { + channel: boolean; + desktop: string; + desktop_sound: boolean; + email: boolean; + first_name: boolean; + mention_keys: string; + push: string; + auto_responder_active: boolean; + auto_responder_message: string; + desktop_notification_sound: string; // Not in use by the mobile app + push_status: string; + comments: string; + }; + position?: string; + props: UserProps; + roles: string; + timezone: { + useAutomaticTimezone: string; + manualTimezone: string; + automaticTimezone: string; + }; + terms_of_service_create_at?: number; + terms_of_service_id?: string; + update_at: number; + username: string; +}; + +export type RawValue = + | RawInfo + | RawChannel + | RawChannelInfo + | RawChannelMembership + | RawCustomEmoji + | RawDraft + | RawFile + | RawGlobal + | RawGroup + | RawGroupMembership + | RawGroupsInChannel + | RawGroupsInTeam + | RawMyChannel + | RawMyChannelSettings + | RawMyTeam + | RawPost + | RawPostMetadata + | RawPostsInChannel + | RawPostsInThread + | RawPreference + | RawReaction + | RawRole + | RawServers + | RawSlashCommand + | RawSystem + | RawTeam + | RawTeamChannelHistory + | RawTeamMembership + | RawTeamSearchHistory + | RawTermsOfService + | RawUser; diff --git a/types/database/enums.ts b/types/database/enums.ts index 3c4b4d8f6d..d0889b2d4a 100644 --- a/types/database/enums.ts +++ b/types/database/enums.ts @@ -7,16 +7,6 @@ export enum OperationType { DELETE = 'DELETE', } -export enum IsolatedEntities { - APP = 'app', - CUSTOM_EMOJI = 'CustomEmoji', - GLOBAL = 'global', - SERVERS = 'servers', - ROLE = 'Role', - SYSTEM = 'System', - TERMS_OF_SERVICE = 'TermsOfService', -} - // The only two types of databases in the app export enum DatabaseType { DEFAULT, diff --git a/types/database/global.d.ts b/types/database/models/app/global.d.ts similarity index 94% rename from types/database/global.d.ts rename to types/database/models/app/global.d.ts index d9b55a78a8..02ed201b1a 100644 --- a/types/database/global.d.ts +++ b/types/database/models/app/global.d.ts @@ -8,7 +8,7 @@ import {Model} from '@nozbe/watermelondb'; * 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: string; /** name : The label/key to use to retrieve the special 'value' */ diff --git a/types/database/app.d.ts b/types/database/models/app/info.d.ts similarity index 87% rename from types/database/app.d.ts rename to types/database/models/app/info.d.ts index add014bc09..fd3694d582 100644 --- a/types/database/app.d.ts +++ b/types/database/models/app/info.d.ts @@ -7,8 +7,8 @@ import {Model} from '@nozbe/watermelondb'; * 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 */ +export default class Info extends Model { + /** table (name) : app */ static table: string; /** build_number : Build number for the app */ diff --git a/types/database/servers.d.ts b/types/database/models/app/servers.d.ts similarity index 96% rename from types/database/servers.d.ts rename to types/database/models/app/servers.d.ts index 63ad6b61fc..c0a31103a8 100644 --- a/types/database/servers.d.ts +++ b/types/database/models/app/servers.d.ts @@ -8,7 +8,7 @@ import {Model} from '@nozbe/watermelondb'; * 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: string; /** db_path : The file path where the database is stored */ diff --git a/types/database/channel.d.ts b/types/database/models/servers/channel.d.ts similarity index 77% rename from types/database/channel.d.ts rename to types/database/models/servers/channel.d.ts index 2c456a06d7..a97d856770 100644 --- a/types/database/channel.d.ts +++ b/types/database/models/servers/channel.d.ts @@ -4,25 +4,25 @@ import {Query, Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -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 './channel_info'; +import ChannelMembership from './channel_membership'; +import Draft from './draft'; +import GroupsInChannel from './groups_in_channel'; +import MyChannel from './my_channel'; +import MyChannelSettings from './my_channel_settings'; +import Post from './post'; +import PostsInChannel from './posts_in_channel'; +import Team from './team'; +import User from './user'; /** * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** create_at : The creation date for this channel */ @@ -73,7 +73,7 @@ export default class Channel extends Model { /** creator : The USER who created this CHANNEL*/ creator: Relation; - /** info : Query returning extra information about this channel from entity CHANNEL_INFO */ + /** info : Query returning extra information about this channel from the CHANNEL_INFO table */ info: Query; /** membership : Query returning the membership data for the current user if it belongs to this channel */ diff --git a/types/database/channel_info.d.ts b/types/database/models/servers/channel_info.d.ts similarity index 78% rename from types/database/channel_info.d.ts rename to types/database/models/servers/channel_info.d.ts index 49af5017c5..8b163cffb7 100644 --- a/types/database/channel_info.d.ts +++ b/types/database/models/servers/channel_info.d.ts @@ -4,18 +4,18 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import Channel from '@typings/database/channel'; +import Channel from './channel'; /** - * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** channel_id : The foreign key from CHANNEL */ @@ -31,11 +31,11 @@ export default class ChannelInfo extends Model { memberCount: number; /** pinned_post_count : The number of post pinned in this channel */ - pinned_post_count: number; + pinnedPostCount: number; /** purpose: The intention behind this channel */ purpose: string; - /** channel : The lazy query property to the record from entity CHANNEL */ + /** channel : The lazy query property to the record from the CHANNEL table */ channel: Relation; } diff --git a/types/database/channel_membership.d.ts b/types/database/models/servers/channel_membership.d.ts similarity index 85% rename from types/database/channel_membership.d.ts rename to types/database/models/servers/channel_membership.d.ts index 082a62caa0..7b9c139938 100644 --- a/types/database/channel_membership.d.ts +++ b/types/database/models/servers/channel_membership.d.ts @@ -4,18 +4,18 @@ import {Query, Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import Channel from '@typings/database/channel'; -import User from '@typings/database/user'; +import Channel from './channel'; +import User from './user'; /** * The ChannelMembership model represents the 'association table' where many channels have users and many users are on * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** channel_id : The foreign key to the related Channel record */ diff --git a/types/database/config.ts b/types/database/models/servers/config.ts similarity index 100% rename from types/database/config.ts rename to types/database/models/servers/config.ts diff --git a/types/database/custom_emoji.d.ts b/types/database/models/servers/custom_emoji.d.ts similarity index 89% rename from types/database/custom_emoji.d.ts rename to types/database/models/servers/custom_emoji.d.ts index 41e8ecfc24..2a3586141b 100644 --- a/types/database/custom_emoji.d.ts +++ b/types/database/models/servers/custom_emoji.d.ts @@ -5,7 +5,7 @@ import {Model} from '@nozbe/watermelondb'; /** 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: string; /** name : The custom emoji's name*/ diff --git a/types/database/draft.d.ts b/types/database/models/servers/draft.d.ts similarity index 87% rename from types/database/draft.d.ts rename to types/database/models/servers/draft.d.ts index dd81320fb2..91c3173f47 100644 --- a/types/database/draft.d.ts +++ b/types/database/models/servers/draft.d.ts @@ -7,10 +7,10 @@ import Model, {Associations} from '@nozbe/watermelondb/Model'; * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** channel_id : The foreign key pointing to the channel in which the draft was made */ @@ -22,6 +22,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 */ rootId: string; - /** files : The files field will hold an array of files object that have not yet been uploaded and persisted within the FILE entity */ + /** files : The files field will hold an array of files object that have not yet been uploaded and persisted within the FILE table */ files: FileInfo[]; } diff --git a/types/database/file.d.ts b/types/database/models/servers/file.d.ts similarity index 89% rename from types/database/file.d.ts rename to types/database/models/servers/file.d.ts index 1df3391a10..d9100f2b7c 100644 --- a/types/database/file.d.ts +++ b/types/database/models/servers/file.d.ts @@ -4,16 +4,16 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import Post from '@typings/database/post'; +import Post from './post'; /** * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** extension : The file's extension */ diff --git a/types/database/group.d.ts b/types/database/models/servers/group.d.ts similarity index 78% rename from types/database/group.d.ts rename to types/database/models/servers/group.d.ts index a4213c501d..3954455a19 100644 --- a/types/database/group.d.ts +++ b/types/database/models/servers/group.d.ts @@ -3,9 +3,9 @@ import Model, {Associations} from '@nozbe/watermelondb/Model'; -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 './group_membership'; +import GroupsInChannel from './groups_in_channel'; +import GroupsInTeam from './groups_in_team'; /** * The Group model unifies/assembles users, teams and channels based on a common ground. For example, a group can be @@ -13,10 +13,10 @@ import GroupsInTeam from '@typings/database/groups_in_team'; * name in the message. (e.g @mobile_team) */ export default class Group extends Model { - /** table (entity name) : Group */ + /** table (name) : Group */ static table: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** display_name : The display name for the group */ diff --git a/types/database/group_membership.d.ts b/types/database/models/servers/group_membership.d.ts similarity index 82% rename from types/database/group_membership.d.ts rename to types/database/models/servers/group_membership.d.ts index 70d88d7775..3c4bfde62c 100644 --- a/types/database/group_membership.d.ts +++ b/types/database/models/servers/group_membership.d.ts @@ -4,18 +4,18 @@ import {Query, Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import Group from '@typings/database/group'; -import User from '@typings/database/user'; +import Group from './group'; +import User from './user'; /** * The GroupMembership model represents the 'association table' where many groups have users and many users are in * groups (relationship type N:N) */ export default class GroupMembership extends Model { - /** table (entity name) : GroupMembership */ + /** table (name) : GroupMembership */ static table: string; - /** associations : Describes every relationship to this entity */ + /** associations : Describes every relationship to this table */ static associations: Associations; groupId: string; userId: string; diff --git a/types/database/groups_in_channel.d.ts b/types/database/models/servers/groups_in_channel.d.ts similarity index 82% rename from types/database/groups_in_channel.d.ts rename to types/database/models/servers/groups_in_channel.d.ts index c109a1e20f..8341c8d87d 100644 --- a/types/database/groups_in_channel.d.ts +++ b/types/database/models/servers/groups_in_channel.d.ts @@ -4,17 +4,17 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import Channel from '@typings/database/channel'; -import Group from '@typings/database/group'; +import Channel from './channel'; +import Group from './group'; /** * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** channel_id : The foreign key of the related CHANNEL model */ diff --git a/types/database/groups_in_team.d.ts b/types/database/models/servers/groups_in_team.d.ts similarity index 81% rename from types/database/groups_in_team.d.ts rename to types/database/models/servers/groups_in_team.d.ts index 9b16de015d..95ac18bc53 100644 --- a/types/database/groups_in_team.d.ts +++ b/types/database/models/servers/groups_in_team.d.ts @@ -4,17 +4,17 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import Group from '@typings/database/group'; -import Team from '@typings/database/team'; +import Group from './group'; +import Team from './team'; /** * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** group_id : The foreign key to the related Group record */ diff --git a/types/database/license.ts b/types/database/models/servers/license.ts similarity index 100% rename from types/database/license.ts rename to types/database/models/servers/license.ts diff --git a/types/database/my_channel.d.ts b/types/database/models/servers/my_channel.d.ts similarity index 82% rename from types/database/my_channel.d.ts rename to types/database/models/servers/my_channel.d.ts index bb91595bcf..625fda1d3c 100644 --- a/types/database/my_channel.d.ts +++ b/types/database/models/servers/my_channel.d.ts @@ -4,16 +4,16 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import Channel from '@typings/database/channel'; +import Channel from './channel'; /** * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** channel_id : The foreign key to the related Channel record */ @@ -34,6 +34,6 @@ export default class MyChannel extends Model { /** roles : The user's privileges on this channel */ roles: string; - /** channel : The relation pointing to entity CHANNEL */ + /** channel : The relation pointing to the CHANNEL table */ channel: Relation; } diff --git a/types/database/my_channel_settings.d.ts b/types/database/models/servers/my_channel_settings.d.ts similarity index 75% rename from types/database/my_channel_settings.d.ts rename to types/database/models/servers/my_channel_settings.d.ts index 7b79ac28e7..61c3ceb251 100644 --- a/types/database/my_channel_settings.d.ts +++ b/types/database/models/servers/my_channel_settings.d.ts @@ -4,17 +4,17 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import Channel from '@typings/database/channel'; +import Channel from './channel'; /** * The MyChannelSettings model represents the specific user's configuration to * the channel this user belongs to. */ export default class MyChannelSettings extends Model { - /** table (entity name) : MyChannelSettings */ + /** table (name) : MyChannelSettings */ static table: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** channel_id : The foreign key to the related CHANNEL record */ @@ -23,6 +23,6 @@ export default class MyChannelSettings extends Model { /** notify_props : Configurations with regards to this channel */ notifyProps: NotifyProps; - /** channel : The relation pointing to entity CHANNEL */ + /** channel : The relation pointing to the CHANNEL table */ channel: Relation; } diff --git a/types/database/my_team.d.ts b/types/database/models/servers/my_team.d.ts similarity index 73% rename from types/database/my_team.d.ts rename to types/database/models/servers/my_team.d.ts index f0b6790c75..6f5fcb5013 100644 --- a/types/database/my_team.d.ts +++ b/types/database/models/servers/my_team.d.ts @@ -4,16 +4,16 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import Team from '@typings/database/team'; +import Team from './team'; /** * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** is_unread : Boolean flag for unread messages on team level */ @@ -25,9 +25,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. */ roles: string; - /** team_id : The foreign key of the 'parent' Team entity */ + /** team_id : The foreign key of the 'parent' Team table */ teamId: string; - /** team : The relation to the entity TEAM, that this user belongs to */ + /** team : The relation to the TEAM table, that this user belongs to */ team: Relation; } diff --git a/types/database/post.d.ts b/types/database/models/servers/post.d.ts similarity index 84% rename from types/database/post.d.ts rename to types/database/models/servers/post.d.ts index 738c1b652a..d0d0e1c210 100644 --- a/types/database/post.d.ts +++ b/types/database/models/servers/post.d.ts @@ -4,22 +4,22 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -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 './channel'; +import Draft from './draft'; +import File from './file'; +import PostInThread from './posts_in_thread'; +import PostMetadata from './post_metadata'; +import Reaction from './reaction'; +import User from './user'; /** * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** channel_id : The foreign key for the Channel to which this post belongs to. */ diff --git a/types/database/post_metadata.d.ts b/types/database/models/servers/post_metadata.d.ts similarity index 79% rename from types/database/post_metadata.d.ts rename to types/database/models/servers/post_metadata.d.ts index 116a53e800..297bde744b 100644 --- a/types/database/post_metadata.d.ts +++ b/types/database/models/servers/post_metadata.d.ts @@ -3,18 +3,17 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import {PostMetadataTypes} from '@typings/database/database'; -import Post from '@typings/database/post'; +import Post from './post'; /** * PostMetadata provides additional information on a POST */ export default class PostMetadata extends Model { - /** table (entity name) : PostMetadata */ + /** table (name) : PostMetadata */ static table: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** post_id : The foreign key of the parent POST model */ diff --git a/types/database/posts_in_channel.d.ts b/types/database/models/servers/posts_in_channel.d.ts similarity index 84% rename from types/database/posts_in_channel.d.ts rename to types/database/models/servers/posts_in_channel.d.ts index 9e2de5a171..2154207f3b 100644 --- a/types/database/posts_in_channel.d.ts +++ b/types/database/models/servers/posts_in_channel.d.ts @@ -4,17 +4,17 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import Channel from '@typings/database/channel'; +import Channel from './channel'; /** * PostsInChannel model helps us to combine adjacent posts together without leaving * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** channel_id : The foreign key of the related parent channel */ diff --git a/types/database/posts_in_thread.d.ts b/types/database/models/servers/posts_in_thread.d.ts similarity index 83% rename from types/database/posts_in_thread.d.ts rename to types/database/models/servers/posts_in_thread.d.ts index 982a09b6f4..b4b24adca5 100644 --- a/types/database/posts_in_thread.d.ts +++ b/types/database/models/servers/posts_in_thread.d.ts @@ -4,17 +4,17 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import Post from '@typings/database/post'; +import Post from './post'; /** * PostsInThread model helps us to combine adjacent threads together without leaving * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** earliest : Lower bound of a timestamp range */ diff --git a/types/database/preference.d.ts b/types/database/models/servers/preference.d.ts similarity index 85% rename from types/database/preference.d.ts rename to types/database/models/servers/preference.d.ts index b631b3db56..b087d9529b 100644 --- a/types/database/preference.d.ts +++ b/types/database/models/servers/preference.d.ts @@ -4,17 +4,17 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import User from '@typings/database/user'; +import User from './user'; /** * The Preference model hold information about the user's preference in the app. * This includes settings about the account, the themes, etc. */ export default class Preference extends Model { - /** table (entity name) : Preference */ + /** table (name) : Preference */ static table: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** category : The preference category ( e.g. Themes, Account settings etc..) */ diff --git a/types/database/reaction.d.ts b/types/database/models/servers/reaction.d.ts similarity index 83% rename from types/database/reaction.d.ts rename to types/database/models/servers/reaction.d.ts index 8bc4aabd82..227c85052d 100644 --- a/types/database/reaction.d.ts +++ b/types/database/models/servers/reaction.d.ts @@ -4,17 +4,17 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import User from '@typings/database/user'; -import Post from '@typings/database/post'; +import User from './user'; +import Post from './post'; /** * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** create_at : Creation timestamp used for sorting reactions amongst users on a particular post */ diff --git a/types/database/role.d.ts b/types/database/models/servers/role.d.ts similarity index 92% rename from types/database/role.d.ts rename to types/database/models/servers/role.d.ts index ef9011ca98..4b9f541ac3 100644 --- a/types/database/role.d.ts +++ b/types/database/models/servers/role.d.ts @@ -5,7 +5,7 @@ import {Model} from '@nozbe/watermelondb'; /** 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: string; /** name : The role's name */ diff --git a/types/database/slash_command.d.ts b/types/database/models/servers/slash_command.d.ts similarity index 89% rename from types/database/slash_command.d.ts rename to types/database/models/servers/slash_command.d.ts index 3d5b94fdbc..c6762717c3 100644 --- a/types/database/slash_command.d.ts +++ b/types/database/models/servers/slash_command.d.ts @@ -4,16 +4,16 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import Team from '@typings/database/team'; +import Team from './team'; /** * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** is_auto_complete : Boolean flag for auto-completing slash commands */ diff --git a/types/database/system.d.ts b/types/database/models/servers/system.d.ts similarity index 93% rename from types/database/system.d.ts rename to types/database/models/servers/system.d.ts index f673de0826..83cdd9526a 100644 --- a/types/database/system.d.ts +++ b/types/database/models/servers/system.d.ts @@ -9,7 +9,7 @@ import {Model} from '@nozbe/watermelondb'; * custom data (e.g. recent emoji used) */ export default class System extends Model { - /** table (entity name) : System */ + /** table (name) : System */ static table: string; /** name : The name or key value for the config */ diff --git a/types/database/team.d.ts b/types/database/models/servers/team.d.ts similarity index 80% rename from types/database/team.d.ts rename to types/database/models/servers/team.d.ts index 4619763f21..8edc11a6cb 100644 --- a/types/database/team.d.ts +++ b/types/database/models/servers/team.d.ts @@ -4,22 +4,22 @@ import {Query} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -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 './channel'; +import GroupsInTeam from './groups_in_team'; +import MyTeam from './my_team'; +import SlashCommand from './slash_command'; +import TeamChannelHistory from './team_channel_history'; +import TeamMembership from './team_membership'; +import TeamSearchHistory from './team_search_history'; /** * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** is_allow_open_invite : Boolean flag indicating if this team is open to the public */ diff --git a/types/database/team_channel_history.d.ts b/types/database/models/servers/team_channel_history.d.ts similarity index 82% rename from types/database/team_channel_history.d.ts rename to types/database/models/servers/team_channel_history.d.ts index c0744880ce..61181689a1 100644 --- a/types/database/team_channel_history.d.ts +++ b/types/database/models/servers/team_channel_history.d.ts @@ -4,17 +4,17 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import Team from '@typings/database/team'; +import Team from './team'; /** * The TeamChannelHistory model helps keeping track of the last channel visited * by the user. */ export default class TeamChannelHistory extends Model { - /** table (entity name) : TeamChannelHistory */ + /** table (name) : TeamChannelHistory */ static table: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** team_id : The foreign key to the related Team record */ diff --git a/types/database/team_membership.d.ts b/types/database/models/servers/team_membership.d.ts similarity index 84% rename from types/database/team_membership.d.ts rename to types/database/models/servers/team_membership.d.ts index 1965c97ff9..9afbb0f782 100644 --- a/types/database/team_membership.d.ts +++ b/types/database/models/servers/team_membership.d.ts @@ -4,18 +4,18 @@ import {Query, Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import User from '@typings/database/user'; -import Team from '@typings/database/team'; +import User from './user'; +import Team from './team'; /** * The TeamMembership model represents the 'association table' where many teams have users and many users are in * teams (relationship type N:N) */ export default class TeamMembership extends Model { - /** table (entity name) : TeamMembership */ + /** table (name) : TeamMembership */ static table: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** team_id : The foreign key to the related Team record */ diff --git a/types/database/team_search_history.d.ts b/types/database/models/servers/team_search_history.d.ts similarity index 85% rename from types/database/team_search_history.d.ts rename to types/database/models/servers/team_search_history.d.ts index 6faef6208b..fb18590081 100644 --- a/types/database/team_search_history.d.ts +++ b/types/database/models/servers/team_search_history.d.ts @@ -4,17 +4,17 @@ import {Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; -import Team from '@typings/database/team'; +import Team from './team'; /** * The TeamSearchHistory model holds the term searched within a team. The searches are performed * at team level in the app. */ export default class TeamSearchHistory extends Model { - /** table (entity name) : TeamSearchHistory */ + /** table (name) : TeamSearchHistory */ static table: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** created_at : The timestamp at which this search was performed */ diff --git a/types/database/terms_of_service.d.ts b/types/database/models/servers/terms_of_service.d.ts similarity index 88% rename from types/database/terms_of_service.d.ts rename to types/database/models/servers/terms_of_service.d.ts index 454c34ad27..78f2610aca 100644 --- a/types/database/terms_of_service.d.ts +++ b/types/database/models/servers/terms_of_service.d.ts @@ -7,7 +7,7 @@ import {Model} from '@nozbe/watermelondb'; * The model for Terms of Service */ export default class TermsOfService extends Model { - /** table (entity name) : TermsOfService */ + /** table (name) : TermsOfService */ static table: string; /** accepted_at : the date the term has been accepted */ diff --git a/types/database/user.d.ts b/types/database/models/servers/user.d.ts similarity index 82% rename from types/database/user.d.ts rename to types/database/models/servers/user.d.ts index 2affeafe7f..3cb6e3d98e 100644 --- a/types/database/user.d.ts +++ b/types/database/models/servers/user.d.ts @@ -3,23 +3,23 @@ import Model, {Associations} from '@nozbe/watermelondb/Model'; -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 './channel'; +import ChannelMembership from './channel_membership'; +import GroupMembership from './group_membership'; +import Post from './post'; +import Preference from './preference'; +import Reaction from './reaction'; +import TeamMembership from './team_membership'; /** - * 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: string; - /** associations : Describes every relationship to this entity. */ + /** associations : Describes every relationship to this table. */ static associations: Associations; /** auth_service : The type of authentication service registered to that user */ diff --git a/types/launch/index.ts b/types/launch/index.ts index 35dade3b78..8f8212e7ab 100644 --- a/types/launch/index.ts +++ b/types/launch/index.ts @@ -49,4 +49,5 @@ export interface LaunchProps { extra?: DeepLinkWithData | NotificationWithData; launchType: LaunchType; launchError?: Boolean; + serverUrl?: string; }