From 5dbbabae57763b91efb51b9432c98d83987fcbbb Mon Sep 17 00:00:00 2001 From: Himself65 Date: Mon, 3 Apr 2023 06:15:39 -0500 Subject: [PATCH] feat: add public workspace page api (#1791) --- apps/web/src/plugins/affine/index.tsx | 4 +- .../src/affine/__tests__/api.spec.ts | 129 ++++++++++++++++-- packages/workspace/src/affine/api/index.ts | 16 ++- 3 files changed, 133 insertions(+), 16 deletions(-) diff --git a/apps/web/src/plugins/affine/index.tsx b/apps/web/src/plugins/affine/index.tsx index ec1abbf781..d91b169d5e 100644 --- a/apps/web/src/plugins/affine/index.tsx +++ b/apps/web/src/plugins/affine/index.tsx @@ -66,9 +66,7 @@ export const AffinePlugin: WorkspacePlugin = { const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate( blockSuiteWorkspace.doc ); - const { id } = await affineApis.createWorkspace( - new Blob([binary.buffer]) - ); + const { id } = await affineApis.createWorkspace(binary); // fixme: syncing images const newWorkspaceId = id; diff --git a/packages/workspace/src/affine/__tests__/api.spec.ts b/packages/workspace/src/affine/__tests__/api.spec.ts index dfd7740fe0..e67db582ef 100644 --- a/packages/workspace/src/affine/__tests__/api.spec.ts +++ b/packages/workspace/src/affine/__tests__/api.spec.ts @@ -7,12 +7,16 @@ import { readFile } from 'node:fs/promises'; import { MessageCode } from '@affine/env/constant'; 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 user2 from '@affine-test/fixtures/built-in-user2.json'; +import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import { assertExists } from '@blocksuite/global/utils'; +import type { Page } from '@blocksuite/store'; import { Workspace } from '@blocksuite/store'; import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { applyUpdate } from 'yjs'; import { createUserApis, @@ -33,6 +37,21 @@ let userApis: ReturnType; let affineAuth: ReturnType; let statusApis: ReturnType; +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 = { name: faker.name.fullName(), email: faker.internet.email(), @@ -47,10 +66,10 @@ beforeEach(() => { }); beforeEach(() => { - affineAuth = createAffineAuth('http://localhost:3000/'); - userApis = createUserApis('http://localhost:3000/'); - workspaceApis = createWorkspaceApis('http://localhost:3000/'); - statusApis = createStatusApis('http://localhost:3000/'); + affineAuth = createAffineAuth('http://127.0.0.1:3000/'); + userApis = createUserApis('http://127.0.0.1:3000/'); + workspaceApis = createWorkspaceApis('http://127.0.0.1:3000/'); + statusApis = createStatusApis('http://127.0.0.1:3000/'); }); beforeEach(async () => { @@ -58,7 +77,7 @@ 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', headers: { 'Content-Type': 'application/json', @@ -80,21 +99,37 @@ declare global { } } +const wsUrl = `ws://127.0.0.1:3000/api/sync/`; + async function createWorkspace( - workspaceApi: typeof workspaceApis + workspaceApi: typeof workspaceApis, + callback?: (workspace: Workspace) => void ): Promise { const workspace = new Workspace({ id: faker.datatype.uuid(), - }); + }) + .register(AffineSchemas) + .register(__unstableSchemas); + if (callback) { + callback(workspace); + } 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(resolve => { + provider.once('status', ({ status }: any) => { + expect(status).toBe('connected'); + resolve(); + }); + }); + } createWorkspaceResponseSchema.parse(data); return data.id; } describe('api', () => { 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', headers: { 'Content-Type': 'application/json', @@ -106,7 +141,7 @@ describe('api', () => { }), }).then(r => r.json()); 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', headers: { 'Content-Type': 'application/json', @@ -121,7 +156,7 @@ describe('api', () => { }); test('failed', async () => { - workspaceApis = createWorkspaceApis('http://localhost:10086/404/'); + workspaceApis = createWorkspaceApis('http://127.0.0.1:10086/404/'); const listener = vi.fn( ( e: CustomEvent<{ @@ -205,7 +240,7 @@ describe('api', () => { const path = require.resolve('@affine-test/fixtures/smile.png'); const imageBuffer = await readFile(path); const binary = Workspace.Y.encodeStateAsUpdate(workspace.doc); - const data = await workspaceApis.createWorkspace(new Blob([binary])); + const data = await workspaceApis.createWorkspace(binary); createWorkspaceResponseSchema.parse(data); const workspaceId = data.id; 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( 'usage', async () => { diff --git a/packages/workspace/src/affine/api/index.ts b/packages/workspace/src/affine/api/index.ts index fc807417ab..028bfb77dd 100644 --- a/packages/workspace/src/affine/api/index.ts +++ b/packages/workspace/src/affine/api/index.ts @@ -233,13 +233,16 @@ export function createWorkspaceApis(prefixUrl = '/') { throw new RequestError(MessageCode.getMembersFailed, e); }); }, - createWorkspace: async (encodedYDoc: Blob): Promise<{ id: string }> => { + createWorkspace: async ( + encodedYDoc: ArrayBuffer + ): Promise<{ id: string }> => { const auth = getLoginStorage(); assertExists(auth); return fetch(prefixUrl + 'api/workspace', { method: 'POST', body: encodedYDoc, headers: { + 'Content-Type': 'application/octet-stream', Authorization: auth.token, }, }) @@ -382,6 +385,17 @@ export function createWorkspaceApis(prefixUrl = '/') { throw new RequestError(MessageCode.leaveWorkspaceFailed, e); }); }, + downloadPublicWorkspacePage: async ( + workspaceId: string, + pageId: string + ): Promise => { + return fetch( + prefixUrl + `api/public/workspace/${workspaceId}/${pageId}`, + { + method: 'GET', + } + ).then(r => r.arrayBuffer()); + }, downloadWorkspace: async ( workspaceId: string, published = false