From cd4e462d8c573466b107bdf8115a7baa2fe34cea Mon Sep 17 00:00:00 2001 From: EYHN Date: Mon, 5 Aug 2024 15:17:19 +0000 Subject: [PATCH] fix(core): transform workspace db when enable cloud (#7744) --- packages/common/infra/src/modules/db/index.ts | 1 + .../infra/src/modules/db/services/db.ts | 27 ++++++++++++++++ .../modules/workspace/providers/flavour.ts | 3 +- .../src/modules/workspace/services/factory.ts | 5 +-- .../modules/workspace/services/transform.ts | 26 +++++++++++++--- .../workspace/testing/testing-provider.ts | 11 +++++-- .../src/hooks/affine/use-enable-cloud.tsx | 10 ++++-- .../modules/workspace-engine/impls/cloud.ts | 10 +++--- .../modules/workspace-engine/impls/local.ts | 6 ++-- tests/affine-cloud/e2e/workspace.spec.ts | 31 +++++++++++++++++++ 10 files changed, 110 insertions(+), 20 deletions(-) diff --git a/packages/common/infra/src/modules/db/index.ts b/packages/common/infra/src/modules/db/index.ts index 9df0747f50442..a122d37931140 100644 --- a/packages/common/infra/src/modules/db/index.ts +++ b/packages/common/infra/src/modules/db/index.ts @@ -6,6 +6,7 @@ import { WorkspaceDBService } from './services/db'; export { AFFiNE_WORKSPACE_DB_SCHEMA } from './schema'; export { WorkspaceDBService } from './services/db'; +export { transformWorkspaceDBLocalToCloud } from './services/db'; export function configureWorkspaceDBModule(framework: Framework) { framework diff --git a/packages/common/infra/src/modules/db/services/db.ts b/packages/common/infra/src/modules/db/services/db.ts index eaffd2050a670..fafbc87ec0ed1 100644 --- a/packages/common/infra/src/modules/db/services/db.ts +++ b/packages/common/infra/src/modules/db/services/db.ts @@ -2,6 +2,7 @@ import { Doc as YDoc } from 'yjs'; import { Service } from '../../../framework'; import { createORMClient, YjsDBAdapter } from '../../../orm'; +import type { DocStorage } from '../../../sync'; import { ObjectPool } from '../../../utils'; import type { WorkspaceService } from '../../workspace'; import { DB, type DBWithTables } from '../entities/db'; @@ -90,3 +91,29 @@ export class WorkspaceDBService extends Service { return docId.startsWith('db$') || docId.startsWith('userdata$'); } } + +export async function transformWorkspaceDBLocalToCloud( + localWorkspaceId: string, + cloudWorkspaceId: string, + localDocStorage: DocStorage, + cloudDocStorage: DocStorage, + accountId: string +) { + for (const tableName of Object.keys(AFFiNE_WORKSPACE_DB_SCHEMA)) { + const localDocName = `db$${localWorkspaceId}$${tableName}`; + const localDoc = await localDocStorage.doc.get(localDocName); + if (localDoc) { + const cloudDocName = `db$${cloudWorkspaceId}$${tableName}`; + await cloudDocStorage.doc.set(cloudDocName, localDoc); + } + } + + for (const tableName of Object.keys(AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA)) { + const localDocName = `userdata$__local__$${localWorkspaceId}$${tableName}`; + const localDoc = await localDocStorage.doc.get(localDocName); + if (localDoc) { + const cloudDocName = `userdata$${accountId}$${cloudWorkspaceId}$${tableName}`; + await cloudDocStorage.doc.set(cloudDocName, localDoc); + } + } +} diff --git a/packages/common/infra/src/modules/workspace/providers/flavour.ts b/packages/common/infra/src/modules/workspace/providers/flavour.ts index bd5516928badc..b12dd3250ee2b 100644 --- a/packages/common/infra/src/modules/workspace/providers/flavour.ts +++ b/packages/common/infra/src/modules/workspace/providers/flavour.ts @@ -28,7 +28,8 @@ export interface WorkspaceFlavourProvider { createWorkspace( initial: ( docCollection: DocCollection, - blobStorage: BlobStorage + blobStorage: BlobStorage, + docStorage: DocStorage ) => Promise ): Promise; diff --git a/packages/common/infra/src/modules/workspace/services/factory.ts b/packages/common/infra/src/modules/workspace/services/factory.ts index 37509fdf8324e..171f4bc0b03c1 100644 --- a/packages/common/infra/src/modules/workspace/services/factory.ts +++ b/packages/common/infra/src/modules/workspace/services/factory.ts @@ -2,7 +2,7 @@ import type { WorkspaceFlavour } from '@affine/env/workspace'; import type { DocCollection } from '@blocksuite/store'; import { Service } from '../../../framework'; -import type { BlobStorage } from '../../../sync'; +import type { BlobStorage, DocStorage } from '../../../sync'; import type { WorkspaceFlavourProvider } from '../providers/flavour'; export class WorkspaceFactoryService extends Service { @@ -20,7 +20,8 @@ export class WorkspaceFactoryService extends Service { flavour: WorkspaceFlavour, initial: ( docCollection: DocCollection, - blobStorage: BlobStorage + blobStorage: BlobStorage, + docStorage: DocStorage ) => Promise = () => Promise.resolve() ) => { const provider = this.providers.find(x => x.flavour === flavour); diff --git a/packages/common/infra/src/modules/workspace/services/transform.ts b/packages/common/infra/src/modules/workspace/services/transform.ts index ef291996578a4..9c2351299edf6 100644 --- a/packages/common/infra/src/modules/workspace/services/transform.ts +++ b/packages/common/infra/src/modules/workspace/services/transform.ts @@ -3,6 +3,7 @@ import { assertEquals } from '@blocksuite/global/utils'; import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { Service } from '../../../framework'; +import { transformWorkspaceDBLocalToCloud } from '../../db'; import type { Workspace } from '../entities/workspace'; import type { WorkspaceMetadata } from '../metadata'; import type { WorkspaceDestroyService } from './destroy'; @@ -18,9 +19,12 @@ export class WorkspaceTransformService extends Service { /** * helper function to transform local workspace to cloud workspace + * + * @param accountId - all local user data will be transformed to this account */ transformLocalToCloud = async ( - local: Workspace + local: Workspace, + accountId: string ): Promise => { assertEquals(local.flavour, WorkspaceFlavour.LOCAL); @@ -28,23 +32,35 @@ export class WorkspaceTransformService extends Service { const newMetadata = await this.factory.create( WorkspaceFlavour.AFFINE_CLOUD, - async (ws, bs) => { - applyUpdate(ws.doc, encodeStateAsUpdate(local.docCollection.doc)); + async (docCollection, blobStorage, docStorage) => { + applyUpdate( + docCollection.doc, + encodeStateAsUpdate(local.docCollection.doc) + ); for (const subdoc of local.docCollection.doc.getSubdocs()) { - for (const newSubdoc of ws.doc.getSubdocs()) { + for (const newSubdoc of docCollection.doc.getSubdocs()) { if (newSubdoc.guid === subdoc.guid) { applyUpdate(newSubdoc, encodeStateAsUpdate(subdoc)); } } } + // transform db + await transformWorkspaceDBLocalToCloud( + local.id, + docCollection.id, + local.engine.doc.storage.behavior, + docStorage, + accountId + ); + const blobList = await local.engine.blob.list(); for (const blobKey of blobList) { const blob = await local.engine.blob.get(blobKey); if (blob) { - await bs.set(blobKey, blob); + await blobStorage.set(blobKey, blob); } } } diff --git a/packages/common/infra/src/modules/workspace/testing/testing-provider.ts b/packages/common/infra/src/modules/workspace/testing/testing-provider.ts index 6d970d9e1caf3..1f525d549d505 100644 --- a/packages/common/infra/src/modules/workspace/testing/testing-provider.ts +++ b/packages/common/infra/src/modules/workspace/testing/testing-provider.ts @@ -6,7 +6,11 @@ import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { Service } from '../../../framework'; import { LiveData } from '../../../livedata'; import { wrapMemento } from '../../../storage'; -import { type BlobStorage, MemoryDocStorage } from '../../../sync'; +import { + type BlobStorage, + type DocStorage, + MemoryDocStorage, +} from '../../../sync'; import { MemoryBlobStorage } from '../../../sync/blob/blob'; import type { GlobalState } from '../../storage'; import type { WorkspaceProfileInfo } from '../entities/profile'; @@ -39,7 +43,8 @@ export class TestingWorkspaceLocalProvider async createWorkspace( initial: ( docCollection: DocCollection, - blobStorage: BlobStorage + blobStorage: BlobStorage, + docStorage: DocStorage ) => Promise ): Promise { const id = nanoid(); @@ -59,7 +64,7 @@ export class TestingWorkspaceLocalProvider }); // apply initial state - await initial(docCollection, blobStorage); + await initial(docCollection, blobStorage, this.docStorage); // save workspace to storage await this.docStorage.doc.set(id, encodeStateAsUpdate(docCollection.doc)); diff --git a/packages/frontend/core/src/hooks/affine/use-enable-cloud.tsx b/packages/frontend/core/src/hooks/affine/use-enable-cloud.tsx index 70ea74ca4ac52..d8873412c96f5 100644 --- a/packages/frontend/core/src/hooks/affine/use-enable-cloud.tsx +++ b/packages/frontend/core/src/hooks/affine/use-enable-cloud.tsx @@ -29,6 +29,8 @@ type ConfirmEnableArgs = [Workspace, ConfirmEnableCloudOptions | undefined]; export const useEnableCloud = () => { const t = useI18n(); + const authService = useService(AuthService); + const account = useLiveData(authService.session.account$); const loginStatus = useLiveData(useService(AuthService).session.status$); const setAuthAtom = useSetAtom(authAtom); const { openConfirmModal, closeConfirmModal } = useConfirmModal(); @@ -39,7 +41,11 @@ export const useEnableCloud = () => { async (ws: Workspace | null, options?: ConfirmEnableCloudOptions) => { try { if (!ws) return; - const { id: newId } = await workspacesService.transformLocalToCloud(ws); + if (!account) return; + const { id: newId } = await workspacesService.transformLocalToCloud( + ws, + account.id + ); openPage(newId, options?.openPageId || WorkspaceSubPath.ALL); options?.onSuccess?.(); } catch (e) { @@ -49,7 +55,7 @@ export const useEnableCloud = () => { }); } }, - [openPage, t, workspacesService] + [account, openPage, t, workspacesService] ); const openSignIn = useCallback(() => { diff --git a/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts b/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts index e9014867dd6e9..18249e44f37f7 100644 --- a/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts +++ b/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts @@ -11,6 +11,7 @@ import { ApplicationStarted, type BlobStorage, catchErrorInto, + type DocStorage, exhaustMapSwitchUntilChanged, fromPromise, type GlobalState, @@ -77,11 +78,10 @@ export class CloudWorkspaceFlavourProviderService async createWorkspace( initial: ( docCollection: DocCollection, - blobStorage: BlobStorage + blobStorage: BlobStorage, + docStorage: DocStorage ) => Promise ): Promise { - const tempId = nanoid(); - // create workspace on cloud, get workspace id const { createWorkspace: { id: workspaceId }, @@ -94,7 +94,7 @@ export class CloudWorkspaceFlavourProviderService const docStorage = this.storageProvider.getDocStorage(workspaceId); const docCollection = new DocCollection({ - id: tempId, + id: workspaceId, idGenerator: () => nanoid(), schema: globalBlockSuiteSchema, blobSources: { @@ -103,7 +103,7 @@ export class CloudWorkspaceFlavourProviderService }); // apply initial state - await initial(docCollection, blobStorage); + await initial(docCollection, blobStorage, docStorage); // save workspace to local storage, should be vary fast await docStorage.doc.set( diff --git a/packages/frontend/core/src/modules/workspace-engine/impls/local.ts b/packages/frontend/core/src/modules/workspace-engine/impls/local.ts index 93465920c740f..5511e31c1fe0b 100644 --- a/packages/frontend/core/src/modules/workspace-engine/impls/local.ts +++ b/packages/frontend/core/src/modules/workspace-engine/impls/local.ts @@ -3,6 +3,7 @@ import { WorkspaceFlavour } from '@affine/env/workspace'; import { DocCollection } from '@blocksuite/store'; import type { BlobStorage, + DocStorage, WorkspaceEngineProvider, WorkspaceFlavourProvider, WorkspaceMetadata, @@ -56,7 +57,8 @@ export class LocalWorkspaceFlavourProvider async createWorkspace( initial: ( docCollection: DocCollection, - blobStorage: BlobStorage + blobStorage: BlobStorage, + docStorage: DocStorage ) => Promise ): Promise { const id = nanoid(); @@ -73,7 +75,7 @@ export class LocalWorkspaceFlavourProvider }); // apply initial state - await initial(docCollection, blobStorage); + await initial(docCollection, blobStorage, docStorage); // save workspace to local storage, should be vary fast await docStorage.doc.set(id, encodeStateAsUpdate(docCollection.doc)); diff --git a/tests/affine-cloud/e2e/workspace.spec.ts b/tests/affine-cloud/e2e/workspace.spec.ts index 31d00f1769ac1..500a36cc4e764 100644 --- a/tests/affine-cloud/e2e/workspace.spec.ts +++ b/tests/affine-cloud/e2e/workspace.spec.ts @@ -5,9 +5,12 @@ import { enableCloudWorkspace, loginUser, } from '@affine-test/kit/utils/cloud'; +import { clickPageModeButton } from '@affine-test/kit/utils/editor'; import { clickNewPageButton, + getBlockSuiteEditorTitle, waitForEditorLoad, + waitForEmptyEditor, } from '@affine-test/kit/utils/page-logic'; import { openSettingModal, @@ -94,3 +97,31 @@ test('should have pagination in member list', async ({ page }) => { await page.waitForTimeout(500); expect(await page.locator('[data-testid="member-item"]').count()).toBe(3); }); + +test('should transform local favorites data', async ({ page }) => { + await page.reload(); + await waitForEditorLoad(page); + await createLocalWorkspace( + { + name: 'test', + }, + page + ); + await page.getByTestId('explorer-bar-add-favorite-button').first().click(); + await clickPageModeButton(page); + await waitForEmptyEditor(page); + + await getBlockSuiteEditorTitle(page).fill('this is a new fav page'); + await expect( + page + .getByTestId('explorer-favorites') + .locator('[draggable] >> text=this is a new fav page') + ).toBeVisible(); + + await enableCloudWorkspace(page); + await expect( + page + .getByTestId('explorer-favorites') + .locator('[draggable] >> text=this is a new fav page') + ).toBeVisible(); +});