fix(core): transform workspace db when enable cloud (#7744)

This commit is contained in:
EYHN
2024-08-05 15:17:19 +00:00
parent a03831f2a2
commit cd4e462d8c
10 changed files with 110 additions and 20 deletions

View File

@@ -6,6 +6,7 @@ import { WorkspaceDBService } from './services/db';
export { AFFiNE_WORKSPACE_DB_SCHEMA } from './schema'; export { AFFiNE_WORKSPACE_DB_SCHEMA } from './schema';
export { WorkspaceDBService } from './services/db'; export { WorkspaceDBService } from './services/db';
export { transformWorkspaceDBLocalToCloud } from './services/db';
export function configureWorkspaceDBModule(framework: Framework) { export function configureWorkspaceDBModule(framework: Framework) {
framework framework

View File

@@ -2,6 +2,7 @@ import { Doc as YDoc } from 'yjs';
import { Service } from '../../../framework'; import { Service } from '../../../framework';
import { createORMClient, YjsDBAdapter } from '../../../orm'; import { createORMClient, YjsDBAdapter } from '../../../orm';
import type { DocStorage } from '../../../sync';
import { ObjectPool } from '../../../utils'; import { ObjectPool } from '../../../utils';
import type { WorkspaceService } from '../../workspace'; import type { WorkspaceService } from '../../workspace';
import { DB, type DBWithTables } from '../entities/db'; import { DB, type DBWithTables } from '../entities/db';
@@ -90,3 +91,29 @@ export class WorkspaceDBService extends Service {
return docId.startsWith('db$') || docId.startsWith('userdata$'); 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);
}
}
}

View File

@@ -28,7 +28,8 @@ export interface WorkspaceFlavourProvider {
createWorkspace( createWorkspace(
initial: ( initial: (
docCollection: DocCollection, docCollection: DocCollection,
blobStorage: BlobStorage blobStorage: BlobStorage,
docStorage: DocStorage
) => Promise<void> ) => Promise<void>
): Promise<WorkspaceMetadata>; ): Promise<WorkspaceMetadata>;

View File

@@ -2,7 +2,7 @@ import type { WorkspaceFlavour } from '@affine/env/workspace';
import type { DocCollection } from '@blocksuite/store'; import type { DocCollection } from '@blocksuite/store';
import { Service } from '../../../framework'; import { Service } from '../../../framework';
import type { BlobStorage } from '../../../sync'; import type { BlobStorage, DocStorage } from '../../../sync';
import type { WorkspaceFlavourProvider } from '../providers/flavour'; import type { WorkspaceFlavourProvider } from '../providers/flavour';
export class WorkspaceFactoryService extends Service { export class WorkspaceFactoryService extends Service {
@@ -20,7 +20,8 @@ export class WorkspaceFactoryService extends Service {
flavour: WorkspaceFlavour, flavour: WorkspaceFlavour,
initial: ( initial: (
docCollection: DocCollection, docCollection: DocCollection,
blobStorage: BlobStorage blobStorage: BlobStorage,
docStorage: DocStorage
) => Promise<void> = () => Promise.resolve() ) => Promise<void> = () => Promise.resolve()
) => { ) => {
const provider = this.providers.find(x => x.flavour === flavour); const provider = this.providers.find(x => x.flavour === flavour);

View File

@@ -3,6 +3,7 @@ import { assertEquals } from '@blocksuite/global/utils';
import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import { Service } from '../../../framework'; import { Service } from '../../../framework';
import { transformWorkspaceDBLocalToCloud } from '../../db';
import type { Workspace } from '../entities/workspace'; import type { Workspace } from '../entities/workspace';
import type { WorkspaceMetadata } from '../metadata'; import type { WorkspaceMetadata } from '../metadata';
import type { WorkspaceDestroyService } from './destroy'; import type { WorkspaceDestroyService } from './destroy';
@@ -18,9 +19,12 @@ export class WorkspaceTransformService extends Service {
/** /**
* helper function to transform local workspace to cloud workspace * helper function to transform local workspace to cloud workspace
*
* @param accountId - all local user data will be transformed to this account
*/ */
transformLocalToCloud = async ( transformLocalToCloud = async (
local: Workspace local: Workspace,
accountId: string
): Promise<WorkspaceMetadata> => { ): Promise<WorkspaceMetadata> => {
assertEquals(local.flavour, WorkspaceFlavour.LOCAL); assertEquals(local.flavour, WorkspaceFlavour.LOCAL);
@@ -28,23 +32,35 @@ export class WorkspaceTransformService extends Service {
const newMetadata = await this.factory.create( const newMetadata = await this.factory.create(
WorkspaceFlavour.AFFINE_CLOUD, WorkspaceFlavour.AFFINE_CLOUD,
async (ws, bs) => { async (docCollection, blobStorage, docStorage) => {
applyUpdate(ws.doc, encodeStateAsUpdate(local.docCollection.doc)); applyUpdate(
docCollection.doc,
encodeStateAsUpdate(local.docCollection.doc)
);
for (const subdoc of local.docCollection.doc.getSubdocs()) { 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) { if (newSubdoc.guid === subdoc.guid) {
applyUpdate(newSubdoc, encodeStateAsUpdate(subdoc)); 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(); const blobList = await local.engine.blob.list();
for (const blobKey of blobList) { for (const blobKey of blobList) {
const blob = await local.engine.blob.get(blobKey); const blob = await local.engine.blob.get(blobKey);
if (blob) { if (blob) {
await bs.set(blobKey, blob); await blobStorage.set(blobKey, blob);
} }
} }
} }

View File

@@ -6,7 +6,11 @@ import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import { Service } from '../../../framework'; import { Service } from '../../../framework';
import { LiveData } from '../../../livedata'; import { LiveData } from '../../../livedata';
import { wrapMemento } from '../../../storage'; 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 { MemoryBlobStorage } from '../../../sync/blob/blob';
import type { GlobalState } from '../../storage'; import type { GlobalState } from '../../storage';
import type { WorkspaceProfileInfo } from '../entities/profile'; import type { WorkspaceProfileInfo } from '../entities/profile';
@@ -39,7 +43,8 @@ export class TestingWorkspaceLocalProvider
async createWorkspace( async createWorkspace(
initial: ( initial: (
docCollection: DocCollection, docCollection: DocCollection,
blobStorage: BlobStorage blobStorage: BlobStorage,
docStorage: DocStorage
) => Promise<void> ) => Promise<void>
): Promise<WorkspaceMetadata> { ): Promise<WorkspaceMetadata> {
const id = nanoid(); const id = nanoid();
@@ -59,7 +64,7 @@ export class TestingWorkspaceLocalProvider
}); });
// apply initial state // apply initial state
await initial(docCollection, blobStorage); await initial(docCollection, blobStorage, this.docStorage);
// save workspace to storage // save workspace to storage
await this.docStorage.doc.set(id, encodeStateAsUpdate(docCollection.doc)); await this.docStorage.doc.set(id, encodeStateAsUpdate(docCollection.doc));

View File

@@ -29,6 +29,8 @@ type ConfirmEnableArgs = [Workspace, ConfirmEnableCloudOptions | undefined];
export const useEnableCloud = () => { export const useEnableCloud = () => {
const t = useI18n(); const t = useI18n();
const authService = useService(AuthService);
const account = useLiveData(authService.session.account$);
const loginStatus = useLiveData(useService(AuthService).session.status$); const loginStatus = useLiveData(useService(AuthService).session.status$);
const setAuthAtom = useSetAtom(authAtom); const setAuthAtom = useSetAtom(authAtom);
const { openConfirmModal, closeConfirmModal } = useConfirmModal(); const { openConfirmModal, closeConfirmModal } = useConfirmModal();
@@ -39,7 +41,11 @@ export const useEnableCloud = () => {
async (ws: Workspace | null, options?: ConfirmEnableCloudOptions) => { async (ws: Workspace | null, options?: ConfirmEnableCloudOptions) => {
try { try {
if (!ws) return; 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); openPage(newId, options?.openPageId || WorkspaceSubPath.ALL);
options?.onSuccess?.(); options?.onSuccess?.();
} catch (e) { } catch (e) {
@@ -49,7 +55,7 @@ export const useEnableCloud = () => {
}); });
} }
}, },
[openPage, t, workspacesService] [account, openPage, t, workspacesService]
); );
const openSignIn = useCallback(() => { const openSignIn = useCallback(() => {

View File

@@ -11,6 +11,7 @@ import {
ApplicationStarted, ApplicationStarted,
type BlobStorage, type BlobStorage,
catchErrorInto, catchErrorInto,
type DocStorage,
exhaustMapSwitchUntilChanged, exhaustMapSwitchUntilChanged,
fromPromise, fromPromise,
type GlobalState, type GlobalState,
@@ -77,11 +78,10 @@ export class CloudWorkspaceFlavourProviderService
async createWorkspace( async createWorkspace(
initial: ( initial: (
docCollection: DocCollection, docCollection: DocCollection,
blobStorage: BlobStorage blobStorage: BlobStorage,
docStorage: DocStorage
) => Promise<void> ) => Promise<void>
): Promise<WorkspaceMetadata> { ): Promise<WorkspaceMetadata> {
const tempId = nanoid();
// create workspace on cloud, get workspace id // create workspace on cloud, get workspace id
const { const {
createWorkspace: { id: workspaceId }, createWorkspace: { id: workspaceId },
@@ -94,7 +94,7 @@ export class CloudWorkspaceFlavourProviderService
const docStorage = this.storageProvider.getDocStorage(workspaceId); const docStorage = this.storageProvider.getDocStorage(workspaceId);
const docCollection = new DocCollection({ const docCollection = new DocCollection({
id: tempId, id: workspaceId,
idGenerator: () => nanoid(), idGenerator: () => nanoid(),
schema: globalBlockSuiteSchema, schema: globalBlockSuiteSchema,
blobSources: { blobSources: {
@@ -103,7 +103,7 @@ export class CloudWorkspaceFlavourProviderService
}); });
// apply initial state // apply initial state
await initial(docCollection, blobStorage); await initial(docCollection, blobStorage, docStorage);
// save workspace to local storage, should be vary fast // save workspace to local storage, should be vary fast
await docStorage.doc.set( await docStorage.doc.set(

View File

@@ -3,6 +3,7 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
import { DocCollection } from '@blocksuite/store'; import { DocCollection } from '@blocksuite/store';
import type { import type {
BlobStorage, BlobStorage,
DocStorage,
WorkspaceEngineProvider, WorkspaceEngineProvider,
WorkspaceFlavourProvider, WorkspaceFlavourProvider,
WorkspaceMetadata, WorkspaceMetadata,
@@ -56,7 +57,8 @@ export class LocalWorkspaceFlavourProvider
async createWorkspace( async createWorkspace(
initial: ( initial: (
docCollection: DocCollection, docCollection: DocCollection,
blobStorage: BlobStorage blobStorage: BlobStorage,
docStorage: DocStorage
) => Promise<void> ) => Promise<void>
): Promise<WorkspaceMetadata> { ): Promise<WorkspaceMetadata> {
const id = nanoid(); const id = nanoid();
@@ -73,7 +75,7 @@ export class LocalWorkspaceFlavourProvider
}); });
// apply initial state // apply initial state
await initial(docCollection, blobStorage); await initial(docCollection, blobStorage, docStorage);
// save workspace to local storage, should be vary fast // save workspace to local storage, should be vary fast
await docStorage.doc.set(id, encodeStateAsUpdate(docCollection.doc)); await docStorage.doc.set(id, encodeStateAsUpdate(docCollection.doc));

View File

@@ -5,9 +5,12 @@ import {
enableCloudWorkspace, enableCloudWorkspace,
loginUser, loginUser,
} from '@affine-test/kit/utils/cloud'; } from '@affine-test/kit/utils/cloud';
import { clickPageModeButton } from '@affine-test/kit/utils/editor';
import { import {
clickNewPageButton, clickNewPageButton,
getBlockSuiteEditorTitle,
waitForEditorLoad, waitForEditorLoad,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic'; } from '@affine-test/kit/utils/page-logic';
import { import {
openSettingModal, openSettingModal,
@@ -94,3 +97,31 @@ test('should have pagination in member list', async ({ page }) => {
await page.waitForTimeout(500); await page.waitForTimeout(500);
expect(await page.locator('[data-testid="member-item"]').count()).toBe(3); 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();
});