mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
feat: add public workspace page api (#1791)
This commit is contained in:
@@ -66,9 +66,7 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
|
|||||||
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate(
|
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate(
|
||||||
blockSuiteWorkspace.doc
|
blockSuiteWorkspace.doc
|
||||||
);
|
);
|
||||||
const { id } = await affineApis.createWorkspace(
|
const { id } = await affineApis.createWorkspace(binary);
|
||||||
new Blob([binary.buffer])
|
|
||||||
);
|
|
||||||
// fixme: syncing images
|
// fixme: syncing images
|
||||||
const newWorkspaceId = id;
|
const newWorkspaceId = id;
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,16 @@ import { readFile } from 'node:fs/promises';
|
|||||||
|
|
||||||
import { MessageCode } from '@affine/env/constant';
|
import { MessageCode } from '@affine/env/constant';
|
||||||
import { createStatusApis } from '@affine/workspace/affine/api/status';
|
import { createStatusApis } from '@affine/workspace/affine/api/status';
|
||||||
|
import type { KeckProvider } from '@affine/workspace/affine/keck';
|
||||||
import user1 from '@affine-test/fixtures/built-in-user1.json';
|
import user1 from '@affine-test/fixtures/built-in-user1.json';
|
||||||
import user2 from '@affine-test/fixtures/built-in-user2.json';
|
import user2 from '@affine-test/fixtures/built-in-user2.json';
|
||||||
|
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
|
import type { Page } from '@blocksuite/store';
|
||||||
import { Workspace } from '@blocksuite/store';
|
import { Workspace } from '@blocksuite/store';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
import { applyUpdate } from 'yjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createUserApis,
|
createUserApis,
|
||||||
@@ -33,6 +37,21 @@ let userApis: ReturnType<typeof createUserApis>;
|
|||||||
let affineAuth: ReturnType<typeof createAffineAuth>;
|
let affineAuth: ReturnType<typeof createAffineAuth>;
|
||||||
let statusApis: ReturnType<typeof createStatusApis>;
|
let statusApis: ReturnType<typeof createStatusApis>;
|
||||||
|
|
||||||
|
function initPage(page: Page) {
|
||||||
|
// Add page block and surface block at root level
|
||||||
|
const pageBlockId = page.addBlock('affine:page', {
|
||||||
|
title: new page.Text(''),
|
||||||
|
});
|
||||||
|
page.addBlock('affine:surface', {}, null);
|
||||||
|
const frameId = page.addBlock('affine:frame', {}, pageBlockId);
|
||||||
|
page.addBlock('affine:paragraph', {}, frameId);
|
||||||
|
page.resetHistory();
|
||||||
|
return {
|
||||||
|
pageBlockId,
|
||||||
|
frameId,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
name: faker.name.fullName(),
|
name: faker.name.fullName(),
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
@@ -47,10 +66,10 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
affineAuth = createAffineAuth('http://localhost:3000/');
|
affineAuth = createAffineAuth('http://127.0.0.1:3000/');
|
||||||
userApis = createUserApis('http://localhost:3000/');
|
userApis = createUserApis('http://127.0.0.1:3000/');
|
||||||
workspaceApis = createWorkspaceApis('http://localhost:3000/');
|
workspaceApis = createWorkspaceApis('http://127.0.0.1:3000/');
|
||||||
statusApis = createStatusApis('http://localhost:3000/');
|
statusApis = createStatusApis('http://127.0.0.1:3000/');
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -58,7 +77,7 @@ beforeEach(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const data = await fetch('http://localhost:3000/api/user/token', {
|
const data = await fetch('http://127.0.0.1:3000/api/user/token', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -80,21 +99,37 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wsUrl = `ws://127.0.0.1:3000/api/sync/`;
|
||||||
|
|
||||||
async function createWorkspace(
|
async function createWorkspace(
|
||||||
workspaceApi: typeof workspaceApis
|
workspaceApi: typeof workspaceApis,
|
||||||
|
callback?: (workspace: Workspace) => void
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const workspace = new Workspace({
|
const workspace = new Workspace({
|
||||||
id: faker.datatype.uuid(),
|
id: faker.datatype.uuid(),
|
||||||
});
|
})
|
||||||
|
.register(AffineSchemas)
|
||||||
|
.register(__unstableSchemas);
|
||||||
|
if (callback) {
|
||||||
|
callback(workspace);
|
||||||
|
}
|
||||||
const binary = Workspace.Y.encodeStateAsUpdate(workspace.doc);
|
const binary = Workspace.Y.encodeStateAsUpdate(workspace.doc);
|
||||||
const data = await workspaceApi.createWorkspace(new Blob([binary]));
|
const data = await workspaceApi.createWorkspace(binary);
|
||||||
|
function waitForConnected(provider: KeckProvider) {
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
provider.once('status', ({ status }: any) => {
|
||||||
|
expect(status).toBe('connected');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
createWorkspaceResponseSchema.parse(data);
|
createWorkspaceResponseSchema.parse(data);
|
||||||
return data.id;
|
return data.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('api', () => {
|
describe('api', () => {
|
||||||
test('built-in mock user', async () => {
|
test('built-in mock user', async () => {
|
||||||
const data = await fetch('http://localhost:3000/api/user/token', {
|
const data = await fetch('http://127.0.0.1:3000/api/user/token', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -106,7 +141,7 @@ describe('api', () => {
|
|||||||
}),
|
}),
|
||||||
}).then(r => r.json());
|
}).then(r => r.json());
|
||||||
loginResponseSchema.parse(data);
|
loginResponseSchema.parse(data);
|
||||||
const data2 = await fetch('http://localhost:3000/api/user/token', {
|
const data2 = await fetch('http://127.0.0.1:3000/api/user/token', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -121,7 +156,7 @@ describe('api', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('failed', async () => {
|
test('failed', async () => {
|
||||||
workspaceApis = createWorkspaceApis('http://localhost:10086/404/');
|
workspaceApis = createWorkspaceApis('http://127.0.0.1:10086/404/');
|
||||||
const listener = vi.fn(
|
const listener = vi.fn(
|
||||||
(
|
(
|
||||||
e: CustomEvent<{
|
e: CustomEvent<{
|
||||||
@@ -205,7 +240,7 @@ describe('api', () => {
|
|||||||
const path = require.resolve('@affine-test/fixtures/smile.png');
|
const path = require.resolve('@affine-test/fixtures/smile.png');
|
||||||
const imageBuffer = await readFile(path);
|
const imageBuffer = await readFile(path);
|
||||||
const binary = Workspace.Y.encodeStateAsUpdate(workspace.doc);
|
const binary = Workspace.Y.encodeStateAsUpdate(workspace.doc);
|
||||||
const data = await workspaceApis.createWorkspace(new Blob([binary]));
|
const data = await workspaceApis.createWorkspace(binary);
|
||||||
createWorkspaceResponseSchema.parse(data);
|
createWorkspaceResponseSchema.parse(data);
|
||||||
const workspaceId = data.id;
|
const workspaceId = data.id;
|
||||||
const blobId = await workspaceApis.uploadBlob(
|
const blobId = await workspaceApis.uploadBlob(
|
||||||
@@ -244,6 +279,76 @@ describe('api', () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test('workspace page binary', async () => {
|
||||||
|
const id = await createWorkspace(workspaceApis, workspace => {
|
||||||
|
{
|
||||||
|
const page = workspace.createPage('page0');
|
||||||
|
const { frameId } = initPage(page);
|
||||||
|
page.addBlock(
|
||||||
|
'affine:paragraph',
|
||||||
|
{
|
||||||
|
text: new page.Text('This is page0'),
|
||||||
|
},
|
||||||
|
frameId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const page = workspace.createPage('page1');
|
||||||
|
const { frameId } = initPage(page);
|
||||||
|
page.addBlock(
|
||||||
|
'affine:paragraph',
|
||||||
|
{
|
||||||
|
text: new page.Text('This is page1'),
|
||||||
|
},
|
||||||
|
frameId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await workspaceApis.updateWorkspace({
|
||||||
|
id,
|
||||||
|
public: true,
|
||||||
|
});
|
||||||
|
{
|
||||||
|
const binary = await workspaceApis.downloadWorkspace(id, false);
|
||||||
|
const workspace = new Workspace({
|
||||||
|
id: faker.datatype.uuid(),
|
||||||
|
})
|
||||||
|
.register(AffineSchemas)
|
||||||
|
.register(__unstableSchemas);
|
||||||
|
applyUpdate(workspace.doc, new Uint8Array(binary));
|
||||||
|
expect(workspace.getPage('page0')).not.toBeUndefined();
|
||||||
|
expect(workspace.getPage('page1')).not.toBeUndefined();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const workspace = new Workspace({
|
||||||
|
id: faker.datatype.uuid(),
|
||||||
|
})
|
||||||
|
.register(AffineSchemas)
|
||||||
|
.register(__unstableSchemas);
|
||||||
|
const binary = await workspaceApis.downloadPublicWorkspacePage(
|
||||||
|
id,
|
||||||
|
'page0'
|
||||||
|
);
|
||||||
|
applyUpdate(workspace.doc, new Uint8Array(binary));
|
||||||
|
expect(workspace.getPage('page0')).not.toBeNull();
|
||||||
|
expect(workspace.getPage('page1')).toBeNull();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const workspace = new Workspace({
|
||||||
|
id: faker.datatype.uuid(),
|
||||||
|
})
|
||||||
|
.register(AffineSchemas)
|
||||||
|
.register(__unstableSchemas);
|
||||||
|
const binary = await workspaceApis.downloadPublicWorkspacePage(
|
||||||
|
id,
|
||||||
|
'page1'
|
||||||
|
);
|
||||||
|
applyUpdate(workspace.doc, new Uint8Array(binary));
|
||||||
|
expect(workspace.getPage('page0')).toBeNull();
|
||||||
|
expect(workspace.getPage('page1')).not.toBeNull();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'usage',
|
'usage',
|
||||||
async () => {
|
async () => {
|
||||||
|
|||||||
@@ -233,13 +233,16 @@ export function createWorkspaceApis(prefixUrl = '/') {
|
|||||||
throw new RequestError(MessageCode.getMembersFailed, e);
|
throw new RequestError(MessageCode.getMembersFailed, e);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
createWorkspace: async (encodedYDoc: Blob): Promise<{ id: string }> => {
|
createWorkspace: async (
|
||||||
|
encodedYDoc: ArrayBuffer
|
||||||
|
): Promise<{ id: string }> => {
|
||||||
const auth = getLoginStorage();
|
const auth = getLoginStorage();
|
||||||
assertExists(auth);
|
assertExists(auth);
|
||||||
return fetch(prefixUrl + 'api/workspace', {
|
return fetch(prefixUrl + 'api/workspace', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: encodedYDoc,
|
body: encodedYDoc,
|
||||||
headers: {
|
headers: {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
Authorization: auth.token,
|
Authorization: auth.token,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -382,6 +385,17 @@ export function createWorkspaceApis(prefixUrl = '/') {
|
|||||||
throw new RequestError(MessageCode.leaveWorkspaceFailed, e);
|
throw new RequestError(MessageCode.leaveWorkspaceFailed, e);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
downloadPublicWorkspacePage: async (
|
||||||
|
workspaceId: string,
|
||||||
|
pageId: string
|
||||||
|
): Promise<ArrayBuffer> => {
|
||||||
|
return fetch(
|
||||||
|
prefixUrl + `api/public/workspace/${workspaceId}/${pageId}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
}
|
||||||
|
).then(r => r.arrayBuffer());
|
||||||
|
},
|
||||||
downloadWorkspace: async (
|
downloadWorkspace: async (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
published = false
|
published = false
|
||||||
|
|||||||
Reference in New Issue
Block a user