diff --git a/lib/utils/migrate-backup-data.js b/lib/utils/migrate-backup-data.js new file mode 100644 --- /dev/null +++ b/lib/utils/migrate-backup-data.js @@ -0,0 +1,178 @@ +// @flow + +import { getConfig } from './config.js'; +import { + translateClientDBMessageInfoToRawMessageInfo, + translateClientDBThreadMessageInfos, +} from './message-ops-utils.js'; +import { createReplaceThreadOperation } from '../ops/create-replace-thread-operation.js'; +import type { EntryStoreOperation } from '../ops/entries-store-ops.js'; +import { createReplaceEntryOperation } from '../ops/entries-store-ops.js'; +import type { MessageStoreOperation } from '../ops/message-store-ops.js'; +import { + createReplaceMessageOperation, + createReplaceMessageStoreLocalOperation, + createReplaceMessageStoreThreadsOperations, +} from '../ops/message-store-ops.js'; +import type { ThreadActivityStoreOperation } from '../ops/thread-activity-store-ops.js'; +import { createReplaceThreadActivityEntryOperation } from '../ops/thread-activity-store-ops.js'; +import type { ThreadStoreOperation } from '../ops/thread-store-ops.js'; +import { messageID } from '../shared/id-utils.js'; +import { databaseIdentifier } from '../types/database-identifier-types.js'; +import type { ThreadMessageInfo } from '../types/message-types.js'; +import type { StoreOperations } from '../types/store-ops-types.js'; +import type { RawThreadInfos } from '../types/thread-types.js'; + +// This function is used only as part of migration 92 to move data +// to new, backup tables. +// This code shouldn't be used in any other use-case. +async function migrateStoreToBackupTables(): Promise { + // 1. Get current client store + const { + threadStore, + threadActivityStore, + entries, + messages, + messageStoreThreads, + messageStoreLocalMessageInfos, + } = await getConfig().sqliteAPI.getClientDBStore(databaseIdentifier.MAIN); + + const threadInfos: RawThreadInfos = threadStore?.threadInfos ?? {}; + + // 2. Prepare ThreadStore operations + const threadIDsToMove: Array = []; + let threadStoreOperations: Array = []; + for (const threadID in threadInfos) { + const op = createReplaceThreadOperation(threadID, threadInfos[threadID]); + // Avoid replacing data already existing in the non-backup table + if (op.payload.isBackedUp) { + threadIDsToMove.push(threadID); + threadStoreOperations.push(op); + } + } + if (threadIDsToMove.length) { + threadStoreOperations = [ + { type: 'remove', payload: { ids: threadIDsToMove } }, + ...threadStoreOperations, + ]; + } + + // 3. Prepare ThreadActivityStore operations + const threadActivityIDsToMove: Array = []; + let threadActivityStoreOperations: Array = []; + for (const threadID in threadActivityStore) { + const op = createReplaceThreadActivityEntryOperation( + threadID, + threadActivityStore[threadID], + threadInfos, + ); + if (op.payload.isBackedUp) { + threadActivityIDsToMove.push(threadID); + threadActivityStoreOperations.push(op); + } + } + if (threadActivityIDsToMove.length) { + threadActivityStoreOperations = [ + { + type: 'remove_thread_activity_entries', + payload: { ids: threadActivityIDsToMove }, + }, + ...threadActivityStoreOperations, + ]; + } + + // 4. Prepare EntryStore operations + const entryIDsToMove: Array = []; + let entryStoreOperations: Array = []; + for (const entryID in entries) { + const op = createReplaceEntryOperation( + entryID, + entries[entryID], + threadInfos, + ); + if (op.payload.isBackedUp) { + entryIDsToMove.push(entryID); + entryStoreOperations.push(op); + } + } + if (entryIDsToMove.length) { + entryStoreOperations = [ + { type: 'remove_entries', payload: { ids: entryIDsToMove } }, + ...entryStoreOperations, + ]; + } + + // 5. Prepare MessageStore operations + const messageIDsToMove: Array = []; + let messageStoreOperations: Array = []; + const rawMessageInfos = + messages?.map(translateClientDBMessageInfoToRawMessageInfo) ?? []; + for (const message of rawMessageInfos) { + const msgID = messageID(message); + const op = createReplaceMessageOperation(msgID, message, threadInfos); + if (op.payload.isBackedUp) { + messageIDsToMove.push(msgID); + messageStoreOperations.push(op); + } + } + if (messageIDsToMove.length) { + messageStoreOperations = [ + { type: 'remove', payload: { ids: messageIDsToMove } }, + ...messageStoreOperations, + ]; + } + + // 5. Prepare MessageStoreThreads operations + const actionPayloadMessageStoreThreads = translateClientDBThreadMessageInfos( + messageStoreThreads ?? [], + ); + const newThreads: { + [threadID: string]: ThreadMessageInfo, + } = {}; + for (const threadID in actionPayloadMessageStoreThreads) { + newThreads[threadID] = { + ...actionPayloadMessageStoreThreads[threadID], + messageIDs: [], + }; + } + const messageStoreThreadsOperations: Array = [ + { type: 'remove_all_threads' }, + ...createReplaceMessageStoreThreadsOperations(newThreads, threadInfos), + ]; + + // 5. Prepare MessageStoreLocal operations + const localMessageIDsToMove: Array = []; + let localMessageStoreOperations: Array = []; + for (const localMessageID in messageStoreLocalMessageInfos) { + const op = createReplaceMessageStoreLocalOperation( + localMessageID, + messageStoreLocalMessageInfos[localMessageID], + ); + if (op.payload.isBackedUp) { + localMessageIDsToMove.push(localMessageID); + localMessageStoreOperations.push(op); + } + } + if (localMessageIDsToMove.length) { + localMessageStoreOperations = [ + { + type: 'remove_local_message_infos', + payload: { ids: localMessageIDsToMove }, + }, + ...localMessageStoreOperations, + ]; + } + + return { + threadStoreOperations, + messageStoreOperations: [ + ...messageStoreOperations, + ...messageStoreThreadsOperations, + ...localMessageStoreOperations, + ], + entryStoreOperations, + threadActivityStoreOperations, + }; +} + +export { migrateStoreToBackupTables }; diff --git a/native/redux/persist-constants.js b/native/redux/persist-constants.js --- a/native/redux/persist-constants.js +++ b/native/redux/persist-constants.js @@ -5,6 +5,6 @@ // NOTE: renaming this constant requires updating // `native/native_rust_library/build.rs` to correctly // scrap Redux state version from this file. -const storeVersion = 91; +const storeVersion = 92; export { rootKey, storeVersion }; diff --git a/native/redux/persist.js b/native/redux/persist.js --- a/native/redux/persist.js +++ b/native/redux/persist.js @@ -123,6 +123,7 @@ translateClientDBMessageInfoToRawMessageInfo, translateRawMessageInfoToClientDBMessageInfo, } from 'lib/utils/message-ops-utils.js'; +import { migrateStoreToBackupTables } from 'lib/utils/migrate-backup-data.js'; import { generateIDSchemaMigrationOpsForDrafts, convertMessageStoreThreadsToNewIDSchema, @@ -1629,6 +1630,13 @@ }, ops: {}, }): MigrationFunction), + [92]: (async (state: AppState) => { + const ops = await migrateStoreToBackupTables(); + return { + state, + ops, + }; + }: MigrationFunction), }); const persistConfig = { diff --git a/web/redux/persist-constants.js b/web/redux/persist-constants.js --- a/web/redux/persist-constants.js +++ b/web/redux/persist-constants.js @@ -3,6 +3,6 @@ const rootKey = 'root'; const rootKeyPrefix = 'persist:'; const completeRootKey = `${rootKeyPrefix}${rootKey}`; -const storeVersion = 91; +const storeVersion = 92; export { rootKey, rootKeyPrefix, completeRootKey, storeVersion }; diff --git a/web/redux/persist.js b/web/redux/persist.js --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -50,6 +50,7 @@ import { parseCookies } from 'lib/utils/cookie-utils.js'; import { isDev } from 'lib/utils/dev-utils.js'; import { stripMemberPermissionsFromRawThreadInfos } from 'lib/utils/member-info-utils.js'; +import { migrateStoreToBackupTables } from 'lib/utils/migrate-backup-data.js'; import { convertDraftStoreToNewIDSchema, createAsyncMigrate, @@ -843,6 +844,13 @@ }, ops: {}, }): MigrationFunction), + [92]: (async (state: AppState) => { + const ops = await migrateStoreToBackupTables(); + return { + state, + ops, + }; + }: MigrationFunction), }; const persistConfig: PersistConfig = {