feat(core): user data db (#7930)

This commit is contained in:
EYHN
2024-09-11 07:55:37 +00:00
parent 498a69af53
commit d93c3b3719
36 changed files with 1220 additions and 281 deletions
@@ -8,6 +8,7 @@ import { configureCommonModules } from '@affine/core/modules';
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header';
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
import { CustomThemeModifier } from '@affine/core/modules/theme-editor';
import { configureSqliteUserspaceStorageProvider } from '@affine/core/modules/userspace';
import { configureDesktopWorkbenchModule } from '@affine/core/modules/workbench';
import {
configureBrowserWorkspaceFlavours,
@@ -88,6 +89,7 @@ configureCommonModules(framework);
configureElectronStateStorageImpls(framework);
configureBrowserWorkspaceFlavours(framework);
configureSqliteWorkspaceEngineStorageProvider(framework);
configureSqliteUserspaceStorageProvider(framework);
configureDesktopWorkbenchModule(framework);
configureAppTabsHeaderModule(framework);
const frameworkProvider = framework.provider();
@@ -1,18 +1,23 @@
import { logger } from '../logger';
import type { SpaceType } from './types';
import type { WorkspaceSQLiteDB } from './workspace-db-adapter';
import { openWorkspaceDatabase } from './workspace-db-adapter';
// export for testing
export const db$Map = new Map<string, Promise<WorkspaceSQLiteDB>>();
export const db$Map = new Map<
`${SpaceType}:${string}`,
Promise<WorkspaceSQLiteDB>
>();
async function getWorkspaceDB(id: string) {
let db = await db$Map.get(id);
if (!db$Map.has(id)) {
const promise = openWorkspaceDatabase(id);
db$Map.set(id, promise);
async function getWorkspaceDB(spaceType: SpaceType, id: string) {
const cacheId = `${spaceType}:${id}` as const;
let db = await db$Map.get(cacheId);
if (!db) {
const promise = openWorkspaceDatabase(spaceType, id);
db$Map.set(cacheId, promise);
const _db = (db = await promise);
const cleanup = () => {
db$Map.delete(id);
db$Map.delete(cacheId);
_db
.destroy()
.then(() => {
@@ -33,6 +38,6 @@ async function getWorkspaceDB(id: string) {
return db!;
}
export function ensureSQLiteDB(id: string) {
return getWorkspaceDB(id);
export function ensureSQLiteDB(spaceType: SpaceType, id: string) {
return getWorkspaceDB(spaceType, id);
}
@@ -1,92 +1,129 @@
import { mainRPC } from '../main-rpc';
import type { MainEventRegister } from '../type';
import { ensureSQLiteDB } from './ensure-db';
import type { SpaceType } from './types';
export * from './ensure-db';
export const dbHandlers = {
getDocAsUpdates: async (workspaceId: string, subdocId: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.getDocAsUpdates(subdocId);
getDocAsUpdates: async (
spaceType: SpaceType,
workspaceId: string,
subdocId: string
) => {
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.getDocAsUpdates(subdocId);
},
applyDocUpdate: async (
spaceType: SpaceType,
workspaceId: string,
update: Uint8Array,
subdocId: string
) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.addUpdateToSQLite(update, subdocId);
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.addUpdateToSQLite(update, subdocId);
},
deleteDoc: async (workspaceId: string, subdocId: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.deleteUpdate(subdocId);
deleteDoc: async (
spaceType: SpaceType,
workspaceId: string,
subdocId: string
) => {
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.deleteUpdate(subdocId);
},
addBlob: async (workspaceId: string, key: string, data: Uint8Array) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.addBlob(key, data);
addBlob: async (
spaceType: SpaceType,
workspaceId: string,
key: string,
data: Uint8Array
) => {
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.addBlob(key, data);
},
getBlob: async (workspaceId: string, key: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.getBlob(key);
getBlob: async (spaceType: SpaceType, workspaceId: string, key: string) => {
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.getBlob(key);
},
deleteBlob: async (workspaceId: string, key: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.deleteBlob(key);
deleteBlob: async (
spaceType: SpaceType,
workspaceId: string,
key: string
) => {
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.deleteBlob(key);
},
getBlobKeys: async (workspaceId: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.getBlobKeys();
getBlobKeys: async (spaceType: SpaceType, workspaceId: string) => {
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.getBlobKeys();
},
getDefaultStorageLocation: async () => {
return await mainRPC.getPath('sessionData');
},
getServerClock: async (workspaceId: string, key: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.adapter.serverClock.get(key);
getServerClock: async (
spaceType: SpaceType,
workspaceId: string,
key: string
) => {
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.adapter.serverClock.get(key);
},
setServerClock: async (
spaceType: SpaceType,
workspaceId: string,
key: string,
data: Uint8Array
) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.adapter.serverClock.set(key, data);
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.adapter.serverClock.set(key, data);
},
getServerClockKeys: async (workspaceId: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.adapter.serverClock.keys();
getServerClockKeys: async (spaceType: SpaceType, workspaceId: string) => {
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.adapter.serverClock.keys();
},
clearServerClock: async (workspaceId: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.adapter.serverClock.clear();
clearServerClock: async (spaceType: SpaceType, workspaceId: string) => {
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.adapter.serverClock.clear();
},
delServerClock: async (workspaceId: string, key: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.adapter.serverClock.del(key);
delServerClock: async (
spaceType: SpaceType,
workspaceId: string,
key: string
) => {
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.adapter.serverClock.del(key);
},
getSyncMetadata: async (workspaceId: string, key: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.adapter.syncMetadata.get(key);
getSyncMetadata: async (
spaceType: SpaceType,
workspaceId: string,
key: string
) => {
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.adapter.syncMetadata.get(key);
},
setSyncMetadata: async (
spaceType: SpaceType,
workspaceId: string,
key: string,
data: Uint8Array
) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.adapter.syncMetadata.set(key, data);
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.adapter.syncMetadata.set(key, data);
},
getSyncMetadataKeys: async (workspaceId: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.adapter.syncMetadata.keys();
getSyncMetadataKeys: async (spaceType: SpaceType, workspaceId: string) => {
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.adapter.syncMetadata.keys();
},
clearSyncMetadata: async (workspaceId: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.adapter.syncMetadata.clear();
clearSyncMetadata: async (spaceType: SpaceType, workspaceId: string) => {
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.adapter.syncMetadata.clear();
},
delSyncMetadata: async (workspaceId: string, key: string) => {
const workspaceDB = await ensureSQLiteDB(workspaceId);
return workspaceDB.adapter.syncMetadata.del(key);
delSyncMetadata: async (
spaceType: SpaceType,
workspaceId: string,
key: string
) => {
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
return spaceDB.adapter.syncMetadata.del(key);
},
};
@@ -0,0 +1 @@
export type SpaceType = 'userspace' | 'workspace';
@@ -6,6 +6,7 @@ import { logger } from '../logger';
import { getWorkspaceMeta } from '../workspace/meta';
import { SQLiteAdapter } from './db-adapter';
import { mergeUpdate } from './merge-update';
import type { SpaceType } from './types';
const TRIM_SIZE = 1;
@@ -121,10 +122,13 @@ export class WorkspaceSQLiteDB {
};
}
export async function openWorkspaceDatabase(workspaceId: string) {
const meta = await getWorkspaceMeta(workspaceId);
const db = new WorkspaceSQLiteDB(meta.mainDBPath, workspaceId);
export async function openWorkspaceDatabase(
spaceType: SpaceType,
spaceId: string
) {
const meta = await getWorkspaceMeta(spaceType, spaceId);
const db = new WorkspaceSQLiteDB(meta.mainDBPath, spaceId);
await db.init();
logger.info(`openWorkspaceDatabase [${workspaceId}]`);
logger.info(`openWorkspaceDatabase [${spaceId}]`);
return db;
}
@@ -6,11 +6,7 @@ import { ensureSQLiteDB } from '../db/ensure-db';
import { logger } from '../logger';
import { mainRPC } from '../main-rpc';
import { storeWorkspaceMeta } from '../workspace';
import {
getWorkspaceDBPath,
getWorkspaceMeta,
getWorkspacesBasePath,
} from '../workspace/meta';
import { getWorkspaceDBPath, getWorkspacesBasePath } from '../workspace/meta';
export type ErrorMessage =
| 'DB_FILE_ALREADY_LOADED'
@@ -45,17 +41,6 @@ export interface FakeDialogResult {
filePaths?: string[];
}
// NOTE:
// we are using native dialogs because HTML dialogs do not give full file paths
export async function revealDBFile(workspaceId: string) {
const meta = await getWorkspaceMeta(workspaceId);
if (!meta) {
return;
}
await mainRPC.showItemInFolder(meta.mainDBPath);
}
// result will be used in the next call to showOpenDialog
// if it is being read once, it will be reset to undefined
let fakeDialogResult: FakeDialogResult | undefined = undefined;
@@ -91,7 +76,7 @@ export async function saveDBFileAs(
workspaceId: string
): Promise<SaveDBFileResult> {
try {
const db = await ensureSQLiteDB(workspaceId);
const db = await ensureSQLiteDB('workspace', workspaceId);
const fakedResult = getFakedResult();
const ret =
@@ -215,7 +200,7 @@ export async function loadDBFile(): Promise<LoadDBFileResult> {
// copy the db file to a new workspace id
const workspaceId = nanoid(10);
const internalFilePath = await getWorkspaceDBPath(workspaceId);
const internalFilePath = await getWorkspaceDBPath('workspace', workspaceId);
await fs.ensureDir(await getWorkspacesBasePath());
await fs.copy(originalPath, internalFilePath);
@@ -1,15 +1,11 @@
import {
loadDBFile,
revealDBFile,
saveDBFileAs,
selectDBFileLocation,
setFakeDialogResult,
} from './dialog';
export const dialogHandlers = {
revealDBFile: async (workspaceId: string) => {
return revealDBFile(workspaceId);
},
loadDBFile: async () => {
return loadDBFile();
},
@@ -8,44 +8,14 @@ import type { WorkspaceMeta } from '../type';
import {
getDeletedWorkspacesBasePath,
getWorkspaceBasePath,
getWorkspaceDBPath,
getWorkspaceMeta,
getWorkspaceMetaPath,
getWorkspacesBasePath,
} from './meta';
import { workspaceSubjects } from './subjects';
export async function listWorkspaces(): Promise<
[workspaceId: string, meta: WorkspaceMeta][]
> {
const basePath = await getWorkspacesBasePath();
try {
await fs.ensureDir(basePath);
const dirs = (
await fs.readdir(basePath, {
withFileTypes: true,
})
).filter(d => d.isDirectory());
const metaList = (
await Promise.all(
dirs.map(async dir => {
// ? shall we put all meta in a single file instead of one file per workspace?
return await getWorkspaceMeta(dir.name);
})
)
).filter((w): w is WorkspaceMeta => !!w);
return metaList.map(meta => [meta.id, meta]);
} catch (error) {
logger.error('listWorkspaces', error);
return [];
}
}
export async function deleteWorkspace(id: string) {
const basePath = await getWorkspaceBasePath(id);
const basePath = await getWorkspaceBasePath('workspace', id);
const movedPath = path.join(await getDeletedWorkspacesBasePath(), `${id}`);
try {
const db = await ensureSQLiteDB(id);
const db = await ensureSQLiteDB('workspace', id);
await db.destroy();
return await fs.move(basePath, movedPath, {
overwrite: true,
@@ -55,52 +25,20 @@ export async function deleteWorkspace(id: string) {
}
}
export async function cloneWorkspace(id: string, newId: string) {
const dbPath = await getWorkspaceDBPath(id);
const newBasePath = await getWorkspaceBasePath(newId);
const newDbPath = await getWorkspaceDBPath(newId);
const metaPath = await getWorkspaceMetaPath(newId);
// check if new workspace dir exists
if (
await fs
.access(newBasePath)
.then(() => true)
.catch(() => false)
) {
throw new Error(`workspace ${newId} already exists`);
}
try {
await fs.ensureDir(newBasePath);
const meta = {
id: newId,
mainDBPath: newDbPath,
};
await fs.writeJSON(metaPath, meta);
await fs.copy(dbPath, newDbPath);
} catch (error) {
logger.error('cloneWorkspace', error);
}
}
export async function storeWorkspaceMeta(
workspaceId: string,
meta: Partial<WorkspaceMeta>
) {
try {
const basePath = await getWorkspaceBasePath(workspaceId);
const basePath = await getWorkspaceBasePath('workspace', workspaceId);
await fs.ensureDir(basePath);
const metaPath = path.join(basePath, 'meta.json');
const currentMeta = await getWorkspaceMeta(workspaceId);
const currentMeta = await getWorkspaceMeta('workspace', workspaceId);
const newMeta = {
...currentMeta,
...meta,
};
await fs.writeJSON(metaPath, newMeta);
workspaceSubjects.meta$.next({
workspaceId,
meta: newMeta,
});
} catch (err) {
logger.error('storeWorkspaceMeta failed', err);
}
@@ -1,27 +1,11 @@
import type { MainEventRegister, WorkspaceMeta } from '../type';
import { cloneWorkspace, deleteWorkspace, listWorkspaces } from './handlers';
import { getWorkspaceMeta } from './meta';
import { workspaceSubjects } from './subjects';
import type { MainEventRegister } from '../type';
import { deleteWorkspace } from './handlers';
export * from './handlers';
export * from './subjects';
export const workspaceEvents = {
onMetaChange: (
fn: (meta: { workspaceId: string; meta: WorkspaceMeta }) => void
) => {
const sub = workspaceSubjects.meta$.subscribe(fn);
return () => {
sub.unsubscribe();
};
},
} satisfies Record<string, MainEventRegister>;
export const workspaceEvents = {} as Record<string, MainEventRegister>;
export const workspaceHandlers = {
list: async () => listWorkspaces(),
delete: async (id: string) => deleteWorkspace(id),
getMeta: async (id: string) => {
return getWorkspaceMeta(id);
},
clone: async (id: string, newId: string) => cloneWorkspace(id, newId),
};
@@ -2,6 +2,7 @@ import path from 'node:path';
import fs from 'fs-extra';
import type { SpaceType } from '../db/types';
import { logger } from '../logger';
import { mainRPC } from '../main-rpc';
import type { WorkspaceMeta } from '../type';
@@ -20,20 +21,39 @@ export async function getWorkspacesBasePath() {
return path.join(await getAppDataPath(), 'workspaces');
}
export async function getWorkspaceBasePath(workspaceId: string) {
return path.join(await getAppDataPath(), 'workspaces', workspaceId);
export async function getWorkspaceBasePath(
spaceType: SpaceType,
workspaceId: string
) {
return path.join(
await getAppDataPath(),
spaceType === 'userspace' ? 'userspaces' : 'workspaces',
workspaceId
);
}
export async function getDeletedWorkspacesBasePath() {
return path.join(await getAppDataPath(), 'deleted-workspaces');
}
export async function getWorkspaceDBPath(workspaceId: string) {
return path.join(await getWorkspaceBasePath(workspaceId), 'storage.db');
export async function getWorkspaceDBPath(
spaceType: SpaceType,
workspaceId: string
) {
return path.join(
await getWorkspaceBasePath(spaceType, workspaceId),
'storage.db'
);
}
export async function getWorkspaceMetaPath(workspaceId: string) {
return path.join(await getWorkspaceBasePath(workspaceId), 'meta.json');
export async function getWorkspaceMetaPath(
spaceType: SpaceType,
workspaceId: string
) {
return path.join(
await getWorkspaceBasePath(spaceType, workspaceId),
'meta.json'
);
}
/**
@@ -41,11 +61,12 @@ export async function getWorkspaceMetaPath(workspaceId: string) {
* This function will also migrate the workspace if needed
*/
export async function getWorkspaceMeta(
spaceType: SpaceType,
workspaceId: string
): Promise<WorkspaceMeta> {
try {
const basePath = await getWorkspaceBasePath(workspaceId);
const metaPath = await getWorkspaceMetaPath(workspaceId);
const basePath = await getWorkspaceBasePath(spaceType, workspaceId);
const metaPath = await getWorkspaceMetaPath(spaceType, workspaceId);
if (
!(await fs
.access(metaPath)
@@ -53,11 +74,12 @@ export async function getWorkspaceMeta(
.catch(() => false))
) {
await fs.ensureDir(basePath);
const dbPath = await getWorkspaceDBPath(workspaceId);
const dbPath = await getWorkspaceDBPath(spaceType, workspaceId);
// create one if not exists
const meta = {
id: workspaceId,
mainDBPath: dbPath,
type: spaceType,
};
await fs.writeJSON(metaPath, meta);
return meta;
@@ -57,16 +57,16 @@ test('can get a valid WorkspaceSQLiteDB', async () => {
'@affine/electron/helper/db/ensure-db'
);
const workspaceId = v4();
const db0 = await ensureSQLiteDB(workspaceId);
const db0 = await ensureSQLiteDB('workspace', workspaceId);
expect(db0).toBeDefined();
expect(db0.workspaceId).toBe(workspaceId);
const db1 = await ensureSQLiteDB(v4());
const db1 = await ensureSQLiteDB('workspace', v4());
expect(db1).not.toBe(db0);
expect(db1.workspaceId).not.toBe(db0.workspaceId);
// ensure that the db is cached
expect(await ensureSQLiteDB(workspaceId)).toBe(db0);
expect(await ensureSQLiteDB('workspace', workspaceId)).toBe(db0);
});
test('db should be destroyed when app quits', async () => {
@@ -74,8 +74,8 @@ test('db should be destroyed when app quits', async () => {
'@affine/electron/helper/db/ensure-db'
);
const workspaceId = v4();
const db0 = await ensureSQLiteDB(workspaceId);
const db1 = await ensureSQLiteDB(v4());
const db0 = await ensureSQLiteDB('workspace', workspaceId);
const db1 = await ensureSQLiteDB('workspace', v4());
expect(db0.adapter).not.toBeNull();
expect(db1.adapter).not.toBeNull();
@@ -94,8 +94,8 @@ test('db should be removed in db$Map after destroyed', async () => {
'@affine/electron/helper/db/ensure-db'
);
const workspaceId = v4();
const db = await ensureSQLiteDB(workspaceId);
const db = await ensureSQLiteDB('workspace', workspaceId);
await db.destroy();
await setTimeout(100);
expect(db$Map.has(workspaceId)).toBe(false);
expect(db$Map.has(`workspace:${workspaceId}`)).toBe(false);
});
@@ -29,7 +29,7 @@ test('can create new db file if not exists', async () => {
'@affine/electron/helper/db/workspace-db-adapter'
);
const workspaceId = v4();
const db = await openWorkspaceDatabase(workspaceId);
const db = await openWorkspaceDatabase('workspace', workspaceId);
const dbPath = path.join(
appDataPath,
`workspaces/${workspaceId}`,
@@ -44,7 +44,7 @@ test('on destroy, check if resources have been released', async () => {
'@affine/electron/helper/db/workspace-db-adapter'
);
const workspaceId = v4();
const db = await openWorkspaceDatabase(workspaceId);
const db = await openWorkspaceDatabase('workspace', workspaceId);
const updateSub = {
complete: vi.fn(),
next: vi.fn(),
@@ -28,40 +28,6 @@ afterAll(() => {
vi.doUnmock('@affine/electron/helper/main-rpc');
});
describe('list workspaces', () => {
test('listWorkspaces (valid)', async () => {
const { listWorkspaces } = await import(
'@affine/electron/helper/workspace/handlers'
);
const workspaceId = v4();
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
const meta = {
id: workspaceId,
};
await fs.ensureDir(workspacePath);
await fs.writeJSON(path.join(workspacePath, 'meta.json'), meta);
const workspaces = await listWorkspaces();
expect(workspaces).toEqual([[workspaceId, meta]]);
});
test('listWorkspaces (without meta json file)', async () => {
const { listWorkspaces } = await import(
'@affine/electron/helper/workspace/handlers'
);
const workspaceId = v4();
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
await fs.ensureDir(workspacePath);
const workspaces = await listWorkspaces();
expect(workspaces).toEqual([
[
workspaceId,
// meta file will be created automatically
{ id: workspaceId, mainDBPath: path.join(workspacePath, 'storage.db') },
],
]);
});
});
describe('delete workspace', () => {
test('deleteWorkspace', async () => {
const { deleteWorkspace } = await import(
@@ -93,7 +59,7 @@ describe('getWorkspaceMeta', () => {
};
await fs.ensureDir(workspacePath);
await fs.writeJSON(path.join(workspacePath, 'meta.json'), meta);
expect(await getWorkspaceMeta(workspaceId)).toEqual(meta);
expect(await getWorkspaceMeta('workspace', workspaceId)).toEqual(meta);
});
test('can create meta if not exists', async () => {
@@ -103,9 +69,10 @@ describe('getWorkspaceMeta', () => {
const workspaceId = v4();
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
await fs.ensureDir(workspacePath);
expect(await getWorkspaceMeta(workspaceId)).toEqual({
expect(await getWorkspaceMeta('workspace', workspaceId)).toEqual({
id: workspaceId,
mainDBPath: path.join(workspacePath, 'storage.db'),
type: 'workspace',
});
expect(
await fs.pathExists(path.join(workspacePath, 'meta.json'))
@@ -124,9 +91,10 @@ describe('getWorkspaceMeta', () => {
await fs.ensureSymlink(sourcePath, path.join(workspacePath, 'storage.db'));
expect(await getWorkspaceMeta(workspaceId)).toEqual({
expect(await getWorkspaceMeta('workspace', workspaceId)).toEqual({
id: workspaceId,
mainDBPath: path.join(workspacePath, 'storage.db'),
type: 'workspace',
});
expect(
@@ -145,6 +113,7 @@ test('storeWorkspaceMeta', async () => {
const meta = {
id: workspaceId,
mainDBPath: path.join(workspacePath, 'storage.db'),
type: 'workspace',
};
await storeWorkspaceMeta(workspaceId, meta);
expect(await fs.readJSON(path.join(workspacePath, 'meta.json'))).toEqual(