mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
fix(core): transform workspace db when enable cloud (#7744)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user